声明函数
函数声明由 func、函数名、参数、返回类型和函数体组成。类型写在参数名之后,而不是之前。
add 接收两个 int 值并返回一个 int。greet 不返回任何值,所以它的签名里没有返回类型,函数体也不需要 return。
声明了返回值的函数必须以终止语句结尾。如果某条路径不经过 return 就能走到右花括号,编译器就会报 missing return 并停止。
共享类型的参数
相邻且类型相同的参数可以共用一个类型名。func add(a, b int) int 和 func add(a int, b int) int 的意思完全相同。也可以混合使用:
func scale(x, y float64, factor int) (float64, float64)
这里 x 和 y 是 float64,factor 是 int。
返回值
函数可以返回零个、一个或多个值。多个返回值写在括号里,调用方用多重赋值接收:
这段代码打印 3 2。(value, error) 这一对是 Go 中最常见的用法,有专门的一页介绍多返回值。
参数会被复制
Go 对每个参数都按值传递。函数操作的是副本,所以给参数赋值永远不会改变调用方的变量。
输出是:
{1 2}
{100 2}
[99 2 3]
第三行常常让人意外。切片值是一个小小的头部(指针、长度、容量)。副本指向同一个底层数组,所以写 s[0] 就会改动调用方的元素。但函数内部的 append 可能会分配一个新数组,而调用方永远看不到这个新的头部。如果函数会让切片变长,就返回新的切片。同样的道理也适用于 map:函数可以添加调用方能看到的键,因为 map 值指向的是共享数据。
当函数必须修改调用方的变量,或者这个值是一个你不想每次调用都复制的大结构体时,使用指针参数。
函数是值
函数有类型,写法是去掉函数名:func(int, int) int。你可以把函数存进变量,放进 map 或切片,也可以把它传给另一个函数。
square 是一个赋值给变量的匿名函数。匿名函数还可以读取和修改外层作用域的变量,这让它们成为闭包。
当同样的函数签名在很多地方出现时,命名的函数类型能让签名更易读:
type Transform func(int) int
func apply(xs []int, f Transform) []int
函数类型的零值是 nil。调用一个 nil 函数会以 invalid memory address or nil pointer dereference panic,所以调用可选回调之前要先检查。函数值只能和 nil 比较,不能互相比较。
递归
函数可以调用自己。Go 没有尾调用优化,但 goroutine 的栈会按需增长,所以普通深度的递归没有问题。
for i := range 6 从 0 数到 5,需要 Go 1.22 或更高版本。
Go 函数没有的特性
- 没有重载。 同一个包中的两个函数不能同名,即使参数不同也不行。
- 没有默认参数或命名参数。 每次调用都要按顺序传入所有参数。配置结构体可以提供具名的可选字段:
NewServer(Config{Port: 8080})。 - 没有嵌套的具名函数。 在函数体内只能声明匿名函数,通常会把它赋值给一个变量。
可变长度的参数列表通过可变参数函数来支持。
导出函数与未导出函数
名字以大写字母开头的函数(Parse)是导出的,可以从其他包调用。小写的名字(parse)只在它自己的包内可见。Go 没有 public 或 private 关键字,规则全看首字母大小写。
常见错误
- 以为对参数的修改会传回调用方。 给参数赋值只会改变副本。返回新值,或者接收一个指针。
- 在函数内部 append 却忽略结果。
func add(s []int) { s = append(s, 1) }对调用方切片的长度毫无影响。写成func add(s []int) []int,然后用s = add(s)。 - 未使用的返回值。 Go 允许忽略返回值。
go vet不会标记被丢弃的错误,但errcheck(golangci-lint的一部分)这类 linter 会。只有在有意忽略时才使用_ =。
常见问题
在 Go 中怎样定义函数?
使用 func 关键字,后面跟函数名、参数列表(类型写在参数名之后)和返回类型:func add(a, b int) int { return a + b }。没有返回值的函数省略类型,有多个返回值的函数把它们写在括号里:func divmod(a, b int) (int, int)。
Go 是值传递还是引用传递?
永远是值传递。函数得到的是每个参数的副本。对于 int 或结构体,这个副本与调用方的变量互不影响。切片、map、channel 和指针是指向共享数据的小值,所以对它们所指向元素的修改,调用方能看到,而给参数本身重新赋值则看不到。当函数必须修改调用方的变量时,传一个指针(*T)。
在 Go 中可以把函数作为参数传递吗?
可以。函数是值,有自己的类型,比如 func(int) int。声明一个该类型的参数,然后传入任何签名匹配的函数,具名函数或匿名函数都行:func apply(xs []int, f func(int) int)。
Go 支持函数重载或默认参数吗?
不支持。同一个包中的每个函数名必须唯一,而且每次调用都必须传入所有参数。常见的替代方案有:使用不同的名字(NewServer、NewServerWithTLS)、配置结构体、可变参数,或者函数式选项。