Go 的 defer,简单但有几个地方得留神
起因
defer 是 Go 里我用得最多的关键字之一,开文件、加锁、关连接,基本每个函数里都有。用起来简单,但有几个行为跟直觉不太一样,踩过之后才记住。
执行顺序是倒着的
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
输出是 third、second、first。后进先出,跟栈一样。这个知道的人多,但实际用的时候还是会忘。比如先 defer file.Close() 再 defer tx.Rollback(),回滚在关闭之后执行,顺序就反了。得按执行顺序倒着写 defer。
参数在 defer 那一刻就求值了
x := 1
defer fmt.Println(x) // 输出 1,不是 2
x = 2
这个坑我踩过。defer 后面的函数参数在声明 defer 的时候就求值了,不是执行的时候。想用最终值,得包一层闭包:
defer func() {
fmt.Println(x) // 输出 2
}()
闭包捕获的是变量本身,不是当时的值。开文件、关连接这种一般不受影响,因为 f 传进去之后不会再变。但涉及循环变量或者后面会改的变量,就得注意。
和命名返回值配合,能改返回值
这个特性知道的人少一点。命名返回值在 defer 里是可以改的:
func do() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("do failed: %w", err)
}
}()
return someFunc()
}
return 先把值赋给 err,然后执行 defer,defer 里改了 err,最终返回的是改过的值。用这个可以统一给错误加包装,省得每个 return 都写一遍。
不用命名返回值的话,defer 里改不了返回值,因为返回值已经确定了。
循环里 defer,容易积攒
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 全部攒到函数结束才关
}
循环里 defer,所有文件句柄都攒到函数返回才关,文件多了可能超限。正确做法是拆成函数,让 defer 在每次迭代结束时就执行:
for _, path := range paths {
if err := readFile(path); err != nil {
return err
}
}
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// ...
}
或者手动关,不靠 defer。循环里我一般不用 defer,除非循环次数很少。
defer 和 recover,只对当前 goroutine 有效
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r)
}
}()
panic("boom")
}()
recover 只能捕获当前 goroutine 的 panic。主 goroutine 里 defer recover 了,子 goroutine panic 了照样把进程带崩。所以每个可能 panic 的 goroutine 入口都得自己加 defer recover。
我现在的习惯是,起 goroutine 的地方统一包一层:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panic: %v\n%s", r, debug.Stack())
}
}()
doWork()
}()
堆栈打出来,不然光知道 panic 了,不知道在哪。
defer 有性能开销,但一般不用管
Go 1.14 之后 defer 做了优化,大部分场景下开销很小,比函数调用多不了多少。只有在极热的循环里,defer 的开销才可能被放大。这种场景本来就该避免在循环里 defer,所以正常写代码不用为了性能刻意不用 defer。
我之前看到有人说“defer 慢,别用”,那是老黄历了。现在该用就用,可读性比那点开销重要。
一个容易忽略的:defer 在 return 之后执行
func do() int {
x := 1
defer func() { x = 2 }()
return x // 返回 1
}
return x 先把 x 的值(1)赋给返回值,然后执行 defer,defer 里改了 x,但返回值已经是 1 了。除非返回值是命名的,否则 defer 改不了。
这个和前面命名返回值那条是一体两面。理解的关键是:return 不是原子操作,它分两步——赋值返回值,执行 defer。
小结
defer 简单,但几个行为得记牢:倒序执行、参数当场求值、命名返回值能改、循环里别攒、recover 只管当前 goroutine。日常写代码该用就用,可读性优先,别为了那点性能把代码写拧巴。
先记这些。
- 点赞
- 收藏
- 关注作者
评论(0)