1. 函数定义
Go
语言最少有个 main() 函数。函数声明告诉了编译器函数的名称,返回类型和参数。
func funcName(parameter_list)(result_list) {
function_body
}
函数定义解析:
-
func:定义函数关键字;
-
funcName:函数名遵循标识符的命名规则,首字母大写其它包可见,首字母小写只能本包可见;
-
parameter_list:参数列表,使用 () 包裹,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数;
-
result_list:返回类型,函数返回一列值。result_list 是该列值的数据类型。有些功能不需要返回值,这种情况下 result_list不是必须的;
-
function_body:函数体使用
{}
包裹,{
必须位于函数定义行的行尾;
2. 函数特点
- 函数可以没有输入参数,也可以没有返回值,默认返回值为 0;
func A() {
// do something
...
}
// or
func A() (int) {
// do something
...
return 1
}
- 多个相邻的相同类型参数可以使用简写模式;
func add(a int, b int) {
...
}
简写为:
func add(a, b int) {
...
}
以下两个声明是等价的:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
- 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回;
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
// sum 相当于函数内的局部变量,被初始化为 0
func add(a, b int) (sum int) {
sum = a + b
return // return sum 的简写模式
// sum := a + b 则相当于新声明一个 sum 变量名,原有的 sum 变量被覆盖
// return sum 需要显式地调用 return sum
}
- 不支持默认值参数;
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时, Go 语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
-
不支持函数重载;
-
函数作为值赋值给变量;
package main
import (
"fmt"
)
func fire() {
fmt.Println("fire")
}
func main() {
var f func()// 将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。
f = fire // 将 fire() 函数作为值,赋给函数变量 f,此时 f 的值为 fire() 函数
f() // 使用函数变量 f 进行函数调用,实际调用的是 fire() 函数。
}
- 不支持命名函数嵌套,但支持匿名函数的嵌套;
func add(a , b int) (sum int) {
anonymous:= func(x , y int) int {
return x + y
}
return anonymous(a , b)
}
- 函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
package main
import "fmt"
func test(fn func() int) int {
return fn()
}
// FormatFunc 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
- 命名返回参数允许 defer 延迟调用通过闭包读取和修改。
package main
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2)) // 输出: 103
}
- 显式 return 返回前,会先修改命名返回参数。
package main
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
println(add(1, 2)) // 输出: 203
}
- 在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、
slice
、map
、function
、channel
等类型,实参可能会由于函数的间接引用被修改。
3. 多值返回
Go
支持多值返回,定义多值返回的返回参数列表时要使用 ()
包含,支持命名参数的返回。
3.1 不带命名参数的返回值
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。使用 return
语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例代码如下:
func swap(a, b int) (int, int) {
return b, a
}
如果多值返回有错误类型,一般将错误类型作为最后一个返回值。
不能用容器对象接收多返回值。只能用多个变量或 “_” 忽略。
package main
func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
多返回值可直接作为其他函数调用实参。
package main
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int { // 不定参数
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
3.2 带命名参数的返回值
Go
语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:
package main
import (
"fmt"
)
func main() {
x, y := namedRetValues()
fmt.Println(x, y) // "5"
}
func namedRetValues() (a, b int) { // 对两个整型返回值进行命名,分别为 a 和 b
a = 1 // 命名返回值的变量与这个函数的局部变量的效果一致,可以对返回值进行赋值和值获取。
b = 2
// 当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的,
return // 等价 return a, b
}
4. 实参形参
Go
函数实参到形参的传递永远是值传递,除非参数传递的是指针值得拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址, 本质上参数传递仍是值拷贝。
package main
import "fmt"
func main() {
a := 10
addOne(a)
fmt.Println("main a is ", a)
fmt.Println("main a address is ", &a)
addPointer(&a) // 实参给形参传递时仍然是值拷贝,传递的是 a 的地址
fmt.Println("main a is ", a)
}
func addOne(a int) int {
a = a + 1
fmt.Println("addOne a is ", a)
return a
}
func addPointer(a *int) {
fmt.Println("addPointer a address is ", a)
*a = *a + 1
fmt.Println("addPointer a address is ", a)
fmt.Println("addPointer a is ", *a)
return
}
输出结果:
addOne a is 11
main a is 10
main a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a is 11
main a is 11
5. 不定参数
不定参数也叫作可变参数,是指函数传入的参数个数是可变的,Go
函数支持不定数目的形式参数,不定参数声明使用 param ...type
的语法格式。
5.1 类型相同的不定参数
为了做到这点,首先需要将函数定义为可以接受可变参数的类型:
package main
import (
"fmt"
)
// 函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int,
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
func main() {
myfunc(1, 2, 3)
myfunc(10, 20, 30)
}
从内部实现机理上来说,类型...type
本质上是一个数组切片,也就是[]type
,通过 reflect.ValueOf(args).Kind()
可以看到其类型为 slice
,这也是为什么上面的参数 args
可以用 for
循环来获得每个传入的参数。
假如没有...type
这样的语法糖,开发者将不得不这么写:
func myfunc2(args []int) {
for _, arg := range args {
fmt.Println(arg)
}
}
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写,但从调用方来说,情形则完全不同:
myfunc2([]int{1, 3, 7, 13})
大家会发现,我们不得不加上[]int{}
来构造一个数组切片实例,但是有了...type
这个语法糖,我们就不用自己来处理了。
函数的不定参数特点:
-
所有的不定参数类型必须是相同的;
-
不定参数必须是函数的最后一个参数;
-
不定参数在函数体内相当于切片,对切片的操作同样适合对不定参数的操作;
-
切片可以作为参数传递给不定参数,切片名后要加上
...
; -
形参为不定参数的函数和形参为切片的函数类型是不相同的;
可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...
,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。
package main
import "fmt"
func main() {
n := []int{1, 2, 3, 4, 5}
result := sumOne(n...) // 切片元素作为参数传递给函数的不定参数,需要在切片名后加上 ...
fmt.Println("result is ", result)
ret := sumTwo(n) // 传递切片自身
fmt.Println("ret is ", ret)
fmt.Printf("sumOne type is %T\n", sumOne) // sumOne type is func(...int) int
fmt.Printf("sumTwo type is %T\n", sumTwo) // sumTwo type is func([]int) int
}
func sumOne(a ...int) (ret int) {
for _, v := range a { // 不定参数相当于切片,可以使用 range 访问
ret += v
}
return
}
func sumTwo(a []int) (ret int) {
for _, v := range a {
ret += v
}
return
}
5.2 类型不同的不定参数
用 interface{}
传递任意类型数据是 Go
语言的惯例用法,使用 interface{}
仍然是类型安全的,下面通过示例来了解一下如何分配传入 interface{}
类型的数据。
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
func main() {
var v1 int = 1
var v2 int64 = 234
var v3 string = "hello"
var v4 float32 = 1.234
MyPrintf(v1, v2, v3, v4)
}
输出结果:
1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.
如果在创建函数时,可变参数存在多种数据类型,则可以将可变参数类型设置成 interface{}
,示例代码如下:
package main
import (
"fmt"
"reflect"
)
func demo(a string, i int, other ...interface{}) {
fmt.Println("first:", a)
fmt.Println("other", i)
fmt.Println("variable parameter:", other, "variable parameter type:", reflect.ValueOf(other).Kind())
for index, val := range other {
fmt.Println("variable parameter index:", index, ",variable parameter value:", val, ",variable parameter type:", reflect.ValueOf(val).Kind())
}
}
func main() {
demo("hello", 10, 100, "b", 3.1415926)
}
输出结果:
first: hello
other 10
variable parameter: [100 b 3.1415926] variable parameter type: slice
variable parameter index: 0 ,variable parameter value: 100 ,variable parameter type: int
variable parameter index: 1 ,variable parameter value: b ,variable parameter type: string
variable parameter index: 2 ,variable parameter value: 3.1415926 ,variable parameter type: float64
5.3 获取可变参数类型
package main
import (
"bytes"
"fmt"
)
func printTypeValue(slist ...interface{}) string {
// 字节缓冲作为快速字符串连接
var b bytes.Buffer
// 遍历参数
for _, s := range slist {
// 将interface{}类型格式化为字符串
str := fmt.Sprintf("%v", s)
// 类型的字符串描述
var typeString string
// 对s进行类型断言
switch s.(type) {
case bool: // 当s为布尔类型时
typeString = "bool"
case string: // 当s为字符串类型时
typeString = "string"
case int: // 当s为整形类型时
typeString = "int"
}
// 写值字符串前缀
b.WriteString("value: ")
// 写入值
b.WriteString(str)
// 写类型前缀
b.WriteString(" type: ")
// 写类型字符串
b.WriteString(typeString)
// 写入换行符,一个输入变量一行
b.WriteString("\n")
}
return b.String()
}
func main() {
// 将不同类型的变量通过printTypeValue打印出来
fmt.Println(printTypeValue(100, "str", true))
}
输出结果:
value: 100 type: int
value: str type: string
value: true type: bool
6. 函数调用
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
Go语言的函数调用格式如下:
返回值变量列表 = 函数名(参数列表)
下面是对各个部分的说明:
- 函数名:需要调用的函数名。
- 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
- 返回值变量列表:多个返回值使用逗号分隔。
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针, slice
(切片)、 map
、 function
、 channel
等类型,实参可能会由于函数的间接引用被修改。
7. 函数作为参数调用
package main
//定义一个函数类型,两个 int 参数,一个 int 返回值
type math func(int, int) int
//定义一个函数 add,这个函数两个 int 参数一个 int 返回值,与 math 类型相符
func add(i int, j int) int {
return i + j
}
//再定义一个 multiply,这个函数同样符合 math 类型
func multiply(i, j int) int {
return i * j
}
//foo 函数,需要一个 math 类型的参数,用 math 类型的函数计算第 2 和第 3 个参数数字,并返回计算结果
//稍后在 main 中我们将 add 函数和 multiply 分别作为参数传递给它
func foo(m math, n1, n2 int) int {
return m(1, 2)
}
func main() {
//传递 add 函数和两个数字,计算相加结果
n := foo(add, 1, 2)
println(n)
//传递 multply 和两个数字,计算相乘结果
n = foo(multiply, 1, 2)
println(n)
}
输出结果
3
2
示例代码
package main
import "fmt"
func funcA(f func() string, s1, s2 string) string {
ret := f()
return ret + s1 + s2
}
func main() {
fu := func() string {
return "hello,world"
}
ret := funcA(fu, " 你好", " 世界")
fmt.Println(ret)
}