跳到主要内容

函数与闭包

问题

Go 函数有哪些特性?defer 的执行顺序是什么?闭包有什么常见陷阱?

答案

函数基础

Go 函数是一等公民——可以赋值给变量、作为参数传递、作为返回值:

// 普通函数
func add(a, b int) int {
return a + b
}

// 多返回值(Go 惯用的 result + error 模式)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

// 命名返回值(裸 return 直接返回命名变量)
func swap(a, b int) (x, y int) {
x, y = b, a
return // 等价于 return x, y
}

// 可变参数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6

// 函数作为值
var fn func(int, int) int = add
fn(1, 2) // 3

// 匿名函数(lambda)
double := func(x int) int { return x * 2 }
double(5) // 10

命名返回值与裸 return

命名返回值会在函数开始时初始化为零值,return 不带参数时返回命名变量的当前值:

func readConfig(path string) (config Config, err error) {
f, err := os.Open(path)
if err != nil {
return // 等价于 return Config{}, err
}
defer f.Close()

err = json.NewDecoder(f).Decode(&config)
return // 返回 config 和 err 的当前值
}
命名返回值 + defer 的陷阱

defer 中可以修改命名返回值,这会影响最终返回结果:

func f() (result int) {
defer func() {
result++ // 修改命名返回值!
}()
return 0 // 实际返回 1,不是 0
}

执行顺序:result = 0defer: result++ → 返回 result(= 1)

defer 详解

defer 将函数调用推迟到当前函数返回前执行。多个 defer 按 LIFO(后进先出) 顺序执行:

func example() {
defer fmt.Println("1st defer") // 第三个执行
defer fmt.Println("2nd defer") // 第二个执行
defer fmt.Println("3rd defer") // 第一个执行
fmt.Println("main logic")
}
// 输出:
// main logic
// 3rd defer
// 2nd defer
// 1st defer

defer 的三条规则

规则 1:defer 的参数在声明时求值

func rule1() {
x := 10
defer fmt.Println(x) // x=10 在此时就确定了
x = 20
// 输出 10,不是 20
}

规则 2:defer 按 LIFO 顺序执行(如上例)

规则 3:defer 可以读写命名返回值

func rule3() (result int) {
defer func() {
result *= 2 // 修改返回值
}()
return 10 // 实际返回 20
}

defer 常用场景

// 1. 资源清理
f, _ := os.Open("file.txt")
defer f.Close()

// 2. 解锁
mu.Lock()
defer mu.Unlock()

// 3. 计时
func trackTime(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}
defer trackTime("processData")()

// 4. recover panic
defer func() {
if r := recover(); r != nil {
log.Printf("recovered: %v", r)
}
}()
defer 在循环中的使用
// ❌ 危险:defer 在函数返回前才执行,循环中的 defer 会堆积
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // 所有文件句柄到函数结束才关闭!
}

// ✅ 正确:提取到独立函数
for _, path := range paths {
processFile(path) // defer 在 processFile 返回时执行
}
func processFile(path string) {
f, _ := os.Open(path)
defer f.Close()
// ...
}

panic 和 recover

panic 触发运行时崩溃,recover 可以在 defer 中捕获 panic:

func safeFunc() {
// recover 必须在 defer 函数中调用
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
// 可以记录日志、返回错误等
}
}()

// 这个 panic 会被 recover 捕获
panic("something went wrong")
}
panic vs error
  • error:预期的、可处理的错误(文件不存在、网络超时),用返回值处理
  • panic:不可恢复的严重错误(数组越界、nil 指针),不要用 panic 做流程控制
  • recover:用于程序的最外层防崩溃(如 HTTP handler 中),不要滥用

闭包

闭包是引用了外部变量的函数。闭包会捕获变量的引用(而非值的副本):

func counter() func() int {
count := 0
return func() int {
count++ // 引用外部变量 count
return count
}
}

c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3

闭包最大的陷阱——循环变量捕获

// ❌ 经典 Bug:所有 goroutine 都打印最后一个值
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 捕获的是变量 i 的引用,不是值
}()
}
// 可能输出 5 5 5 5 5(循环结束时 i=5)

// ✅ 修复方法 1:传参(值拷贝)
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i) // i 作为参数传入,值拷贝
}

// ✅ 修复方法 2:循环内声明新变量
for i := 0; i < 5; i++ {
i := i // 新的局部变量,遮蔽外层 i
go func() {
fmt.Println(i)
}()
}
Go 1.22 修复了循环变量陷阱

从 Go 1.22 开始,for 循环的循环变量在每次迭代中都是新的变量,不再是同一个。这意味着上述 Bug 在 Go 1.22+ 中不会出现。但面试中这仍然是常考点。

函数式编程模式

// 高阶函数
func apply(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = fn(n)
}
return result
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2, 4, 6]

// 柯里化
func adder(x int) func(int) int {
return func(y int) int { return x + y }
}
add5 := adder(5)
add5(3) // 8

常见面试问题

Q1: Go 函数参数是值传递还是引用传递?

答案

Go 的函数参数永远是值传递。所有类型都会被拷贝一份传给函数。不同的是拷贝的内容:

类型拷贝内容能否修改原值
基本类型(int、string...)值本身
数组整个数组(可能很大)
结构体整个结构体
切片slice header(24字节)✅ 能修改元素,不能 append
map指针(8字节)✅ 能增删改
channel指针(8字节)✅ 能发送接收
指针指针本身(8字节)✅ 能修改指向的值

切片、map、channel 虽然是"值拷贝",但拷贝的是内部指针,所以效果类似引用传递。

Q2: defer 的执行顺序是什么?

答案

  1. 参数在 defer 声明时求值
  2. 多个 defer 按 LIFO(栈)顺序执行
  3. deferreturn 之后、函数实际退出之前执行
  4. defer 可以修改命名返回值

执行顺序:return 赋值defer 函数执行函数实际返回

Q3: 以下代码输出什么?

func f() int {
x := 5
defer func() {
x++
}()
return x
}

答案:返回 5。因为返回值不是命名的,return x 会把 x 的值拷贝到返回值,defer 中修改的是局部变量 x,不影响已经拷贝走的返回值。

但如果改为命名返回值:

func f() (x int) {
x = 5
defer func() {
x++
}()
return x // 返回 6!defer 修改了命名返回值
}

Q4: panic 可以被 recover 吗?有什么限制?

答案

  • recover 可以捕获 panic,但必须在 defer 函数中直接调用
  • recover 不在 defer 中调用无效
  • runtime.Goexit() 不能被 recover
  • ❌ 并发 map 读写导致的 fatal error 不能被 recover
  • ❌ 栈溢出(递归过深)不能被 recover
// ❌ 无效:recover 不在 defer 中
panic("err")
recover() // 不会捕获

// ❌ 无效:recover 被 defer 的非直接调用
defer fmt.Println(recover()) // recover 在 Print 的参数中求值,此时没有 panic

// ✅ 有效
defer func() {
if r := recover(); r != nil {
// 成功捕获
}
}()

Q5: Go 1.22 对循环变量做了什么改动?

答案

Go 1.22 之前,for 循环的循环变量在整个循环中是同一个变量,每次迭代修改它的值。这导致经典的闭包 Bug——goroutine 和闭包捕获的都是最后一次迭代的值。

Go 1.22 起,每次迭代都创建新的循环变量(类似 JavaScript 的 let)。这意味着闭包捕获的是每次迭代独立的变量,不再有共享变量问题。

这是一个语义变更(Breaking Change),通过 go.mod 中的 Go 版本号控制——只有 go 1.22 及以上的模块才启用新行为。

相关链接