函数
需要预先了解:
1. 函数
Go 的函数有两个显著特点:参数和返回值的类型都写在名字后面,以及支持多返回值。除此之外,函数在 Go 里是一等公民——可以赋值给变量、作为参数传递、作为返回值,这也让闭包成为日常写法。这篇还会讲清楚一个容易被忽略的事实:Go 的参数传递永远是值拷贝。
1.1 定义与调用
func add(a int, b int) int {
return a + b
}
func是关键字;add是函数名;- 参数列表里,名字在前、类型在后;相邻参数类型相同时可以合并写成
a, b int; - 返回值类型写在参数列表之后。
没有返回值时省略返回类型:
func greet(name string) {
fmt.Println("你好,", name)
}
1.2 多返回值
函数可以返回多个值,这是 Go 里处理"结果 + 错误"的标准形式:
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
不需要的返回值用 _ 丢弃:
n, _ := strconv.Atoi("42")
fmt.Println(n) // 42
多返回值配合逗号 ok 惯例,就构成了 Go 错误处理的基础形态(详见 错误处理)。
1.3 命名返回值
返回值可以起名字,起名后它们就是函数内已声明的变量:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 裸 return,返回当前 x、y 的值
}
x, y := split(17)
fmt.Println(x, y) // 7 10
裸 return(不写返回值)只能用在有命名返回值的函数里。命名返回值有两个实际用途:
- 在文档里说明每个返回值的含义;
- 配合
defer修改返回值:
func double() (result int) {
defer func() { result *= 2 }()
return 10
}
fmt.Println("double() =", double()) // 20
return 10 会先把 10 赋给 result,再执行 defer,defer 里把 result 改成 20,函数最终返回 20。这个机制在 错误处理 里会完整展开。
注:返回值短的时候用命名返回值可以提升可读性,但函数体很长时,命名返回值容易让人忘记"这个变量最后会被返回",反而增加心智负担。
1.4 可变参数
参数类型前加 ... 表示可变参数,函数内它是一个切片:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
调用时可以逐个传,也可以把切片展开:
fmt.Println(sum(1, 2, 3)) // 6
nums := []int{1, 2, 3, 4}
fmt.Println(sum(nums...)) // 10
fmt.Println(sum(empty...)) // 0
fmt.Println(sum()) // 0
可变参数只能有一个,且必须放在参数列表最后:
func logf(level string, args ...any) {
fmt.Print(level, ": ")
fmt.Println(args...)
}
any 是 interface{} 的别名(Go 1.18+),可以接收任意类型的值。
1.5 参数传递:一切都是值拷贝
Go 只有值传递,没有引用传递。但"值"的含义取决于类型:结构体拷的是字段,切片拷的是表头,指针拷的是地址。三类实验放在一起看就清楚了。
type Person struct {
Name string
Age int
}
func modifyStruct(p Person) { p.Name = "函数内改名" } // 改副本
func modifyPtr(p *Person) { p.Name = "函数内改名" } // 改指向的对象
func modifySliceElem(s []int) { s[0] = 100 } // 改共享的底层数组
func appendInFunc(s []int) { s = append(s, 999) } // 只改副本的表头
p := Person{Name: "一撮灰", Age: 3}
modifyStruct(p)
fmt.Println("传值后:", p) // {一撮灰 3}
modifyPtr(&p)
fmt.Println("传指针后:", p) // {函数内改名 3}
s := []int{1, 2, 3}
modifySliceElem(s)
fmt.Println("改元素后:", s) // [100 2 3]
appendInFunc(s)
fmt.Println("函数内 append 后:", s, len(s)) // [100 2 3] 3
结论:
- 结构体传参:拷贝整个结构体,函数内怎么改都不影响调用方;
- 指针传参:拷贝的是地址,通过它修改指向的对象,调用方能看到;
- 切片传参:拷贝的是 24 字节的表头,底层数组共享,所以改元素有效,但函数内
append只改了副本的len。
想让函数内的 append 生效,就要返回新切片由调用方接收:
func push(s []int, v int) []int {
return append(s, v)
}
1.6 函数是一等公民
函数类型由参数和返回值决定,func(int) int 和 func(int) string 是不同类型。可以用 type 给函数类型起名:
type IntOp func(int) int
函数可以赋值给变量、作为参数、作为返回值:
func apply(nums []int, f IntOp) []int {
out := make([]int, 0, len(nums))
for _, n := range nums {
out = append(out, f(n))
}
return out
}
func filter(nums []int, pred func(int) bool) []int { /* ... */ }
func compose(f, g IntOp) IntOp {
return func(n int) int { return f(g(n)) }
}
nums := []int{1, 2, 3, 4, 5}
double := func(n int) int { return n * 2 }
fmt.Println(apply(nums, double)) // [2 4 6 8 10]
fmt.Println(filter(nums, func(n int) bool { return n%2 == 0 })) // [2 4]
inc := func(n int) int { return n + 1 }
fmt.Println(compose(double, inc)(10)) // 22
需要给不同类型的切片做同样的操作时,从 Go 1.18 起可以写泛型函数,比"为每种类型写一遍"或"用 any 加类型断言"更清晰,见 泛型。
1.7 匿名函数与闭包
匿名函数是没有名字的函数,可以直接定义并调用,也可以赋给变量:
double := func(n int) int { return n * 2 }
fmt.Println(double(21)) // 42
闭包(closure)是引用了外部变量的函数。它捕获的是变量本身,不是变量在某一时刻的快照:
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter(), counter(), counter()) // 1 2 3
count 在 makeCounter 返回后依然存活,因为它被闭包引用着(这种情况下 count 会分配在堆上,见 内存分配与逃逸分析)。
多个闭包共享同一个变量时,一个改、另一个能读到:
x := 0
add := func() { x++ }
show := func() int { return x }
add()
add()
fmt.Println("x =", show()) // x = 2
循环变量捕获
在 Go 1.22 之前,for 的循环变量在整个循环中只有一个实例,闭包捕获到的是同一个变量,所有闭包最后都会看到循环结束时的值。Go 1.22 起,每轮迭代都会创建新的循环变量,所以现在这段代码会输出 0 1 2:
funcs := make([]func(), 0, 3)
for i := 0; i < 3; i++ {
funcs = append(funcs, func() { fmt.Print(i, " ") })
}
for _, f := range funcs {
f()
}
0 1 2
在 Go 1.22 之前的版本里,同样的代码会输出 3 3 3。这个差异在并发场景里影响更大,详见 并发设计模式与陷阱总结。
1.8 defer
defer 把一次函数调用推迟到当前函数返回前执行,常用于关闭资源:
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close()
它有两条必须记住的规则。
1. 多个 defer 按 LIFO(后进先出)顺序执行。
for i := 1; i <= 3; i++ {
defer fmt.Println("defer", i)
}
defer 3
defer 2
defer 1
2. defer 后面调用的参数在 defer 语句执行时立即求值。
i := 1
defer fmt.Println("捕获的值:", i) // 这里就把 i 复制成 1
i = 100
defer func() { fmt.Println("闭包看到的值:", i) }() // 闭包捕获变量,看到最新值
fmt.Println("main 结束前 i =", i)
main 结束前 i = 100
闭包看到的值: 100
捕获的值: 1
注意输出顺序:先打印 main 里的语句,再按 LIFO 执行两个 defer,所以闭包那个(后注册)先执行。
注意:
defer在循环里使用时,注册的调用会累积到函数返回时才执行。循环里打开大量文件并defer Close,会导致文件描述符一直占用到函数结束,这种情况下应该把循环体抽成独立函数。
1.9 练习
- 写一个
reduce高阶函数,签名是func reduce(nums []int, init int, f func(int, int) int) int,用它分别实现求和与求最大值。 - 用闭包实现一个
next函数,每次调用返回斐波那契数列的下一个数。 - 写一个函数,用
defer记录函数耗时(配合time.Now()),并验证defer的参数求值时机对结果的影响。



