Menu

Golang 函数:声明、调用与传递函数

如何在 Go 中声明和调用函数:参数、共享类型的参数、返回值、值传递,以及可以存储和传递的函数值。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

声明函数

函数声明由 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)、配置结构体、可变参数,或者函数式选项。

Coddy 编程语言插图

和 Coddy 一起学编程

开始