Go 函数详解 — 多返回值、闭包与 defer
函数是组织代码的基本单位。Go 的函数吸收了 C 家族的简洁,又加入了几项现代语言的招牌特性:多返回值、命名返回值、一等公民函数和 defer 延迟执行。掌握它们,你就能写出地道、健壮的 Go 代码。
一、函数的定义与参数简写
Go 用 func 关键字定义函数,基本形式是 func 名字(参数) 返回类型,后面跟一对花括号 { } 包起来的函数体。和很多语言不同,Go 把类型写在变量名后面——变量在前、类型在后,这是贯穿 Go 全局的约定,习惯了反而觉得清晰。
package main
import "fmt"
// 基本定义:func 函数名(参数列表) 返回类型
func add(a int, b int) int {
return a + b
}
// 参数简写:连续多个同类型参数可合并
func sub(a, b int) int {
return a - b
}
// 多返回值:Go 的招牌特性,常用 (结果, 错误)
func div(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}
func main() {
fmt.Println(add(3, 5), sub(10, 4)) // 8 6
r, err := div(10, 0)
if err != nil {
fmt.Println("出错了:", err)
} else {
fmt.Println("结果:", r)
}
}当多个连续参数类型相同时可以合并:func f(a, b, c int) 等价于 func f(a int, b int, c int)。需要特别提醒——Go 不支持参数默认值,这一点和 Python、JS 不同,通常用可变参数或"函数选项模式"(functional options)来弥补。
二、多返回值与命名返回值
多返回值是 Go 最让人眼前一亮的特性。一个函数可以同时返回"结果"和"错误",调用方必须同时接收,这就把"面对异常"这件事从可选变成必选,极大提升了程序的健壮性。Go 社区的核心约定是:不要用抛异常掩盖错误,而要把错误当成普通返回值,一步一步检查。
命名返回值在返回类型前加括号、给返回值起名字,如 (total int, count int)。好处有两个:一是充当"自带文档",让人一眼看出返回值含义;二是函数末尾可以写"裸 return",自动返回这些命名变量。短函数用裸 return 很简洁,长函数里则建议显式写出返回值,避免读者来回翻找哪个变量被返回。
package main
import "fmt"
// 命名返回值:给返回值起名,可当文档;函数末尾可"裸 return"
func stats(nums ...int) (total int, count int) {
count = len(nums)
for _, n := range nums {
total += n
}
return // 裸 return,自动返回当前的 total 和 count
}
func main() {
// 注意:nums 是可变参数,在函数内是 []int 切片
total, n := stats(1, 2, 3, 4, 5)
fmt.Printf("总和=%d, 个数=%d\n", total, n)
// 输出:总和=15, 个数=5
}三、可变参数
参数名前加 ... 表示可变参数,函数内收到的实际上是一个切片。比如 func f(nums ...int) 里的 nums 类型是 []int。调用时可以传任意数量的参数,也可以把一个已有的切片用 s... 的方式"展开"传入。上面的 stats 函数同时演示了可变参数和命名返回值,仔细看一遍就能融会贯通。
四、匿名函数与闭包
Go 的函数是一等公民:可以赋值给变量、当作参数传递、当作返回值。没有名字的函数叫匿名函数,语法是 func(参数) 返回类型 { 函数体 },定义后立即调用或赋给变量都行。下面这段代码把匿名函数和闭包一次讲透。
package main
import "fmt"
func main() {
// 匿名函数:没有名字,赋值给变量后调用
add := func(a, b int) int {
return a + b
}
fmt.Println(add(2, 3)) // 5
// 立即执行的匿名函数(IIFE)
sq := func(x int) int { return x * x }(6)
fmt.Println(sq) // 36
// 闭包:返回的函数"记住"了外层的 count
c := makeCounter()
fmt.Println(c(), c(), c()) // 1 2 3
}
// makeCounter 返回一个会累加的闭包
func makeCounter() func() int {
count := 0 // 被闭包捕获,逃逸到堆上
return func() int {
count++
return count
}
}闭包(closure)是匿名函数的进阶用法:函数内部引用了外部作用域的变量,这个变量就被"捕获",生命周期跟随闭包一起延长(术语叫"逃逸到堆上")。上面的 makeCounter 每次调用都创建一个独立的 count,返回的函数反复调用会一直累加,而且每次调用 makeCounter 得到的计数器互不干扰。这正是封装私有状态的优雅方式,根本不需要写一个类。
五、defer 延迟调用
defer 后面跟一个函数调用,这个调用会被推迟到外层函数即将返回时才执行。多个 defer 按后进先出(LIFO)的顺序执行——先 defer 的反而后执行,像栈一样。它最常见的用途是资源释放:打开文件后立刻 defer file.Close(),加锁后立刻 defer mutex.Unlock(),保证无论后面走哪个分支、是否中途出错,资源都能被正确释放。
package main
import "fmt"
func main() {
// defer 延迟到函数返回时执行,多个 defer 后进先出(LIFO)
defer fmt.Println("第一个 defer,最后执行")
defer fmt.Println("第二个 defer,先执行")
fmt.Println("正常代码")
// 典型用途:成对的"获取/释放",保证资源被回收
// f, _ := os.Open("a.txt")
// defer f.Close() // 函数返回时一定关闭
}
// 输出顺序:
// 正常代码
// 第二个 defer,先执行
// 第一个 defer,最后执行新手常踩一个坑:defer 语句的参数是在 defer 那一行被执行时就求值的,不是真正调用时。所以 defer fmt.Println(i) 锁定的是当时的 i。另外,在循环里使用 defer 要格外小心——所有 defer 都要等到函数返回才一并执行,循环里大量 defer 可能积压未释放的资源。遇到这种情况,通常把循环体抽成一个独立函数,让资源在每轮结束时立刻释放。
六、递归
Go 完全支持递归——函数直接或间接调用自己。最经典的例子是阶乘和斐波那契数列。写递归有两个要点:必须有一个终止条件(否则会无限递归直到栈溢出),并且每递归一次问题规模要缩小。下面的代码同时给出阶乘和斐波那契,你可以直接拿去运行。
package main
import "fmt"
// 阶乘:n! = n * (n-1)!
func fact(n int) int {
if n <= 1 {
return 1 // 终止条件,防止无限递归
}
return n * fact(n-1)
}
// 斐波那契:F(n) = F(n-1) + F(n-2)
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
fmt.Println(fact(5)) // 120
fmt.Println(fib(10)) // 55
}小提示:朴素的斐波那契递归时间复杂度是 O(2^n),n 稍大就会非常慢,实际工程中一般改成循环或用"记忆化"优化。这里主要是为了演示递归写法。
小结
- 类型后置是 Go 的统一风格,函数参数和返回值都遵守。
- 多返回值让"结果 + 错误"成为标配,务必养成检查 error 的习惯。
- 闭包封装私有状态、defer保证资源释放,两者是写出健壮 Go 代码的利器。
- Go 没有默认参数、也没有函数重载,靠可变参数和选项模式达到类似效果。