深入理解Go语言中的defer:不仅仅是语法糖

本文深入探讨Go语言中defer关键字的语义特性,通过实际代码示例展示defer在异常处理中的重要作用,解释为什么defer不是简单的语法糖,并强调正确使用defer对程序健壮性的关键意义。

Defer: 甜蜜但非语法糖

defer简析 ¶

学习Go语言时,很快会遇到defer关键字。例如,《Go语言之旅》这样介绍defer:

defer语句将函数的执行推迟到周围函数返回时。
延迟调用的参数会立即求值,但函数调用要等到周围函数返回才会执行。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

defer提供了一种便捷的方式,确保某段代码在封闭函数返回控制给调用者之前无条件执行。通常,你会编写获取某些资源(文件描述符、互斥锁等)的函数。这类函数通常必须在返回前释放这些资源,无论执行路径如何——你的函数可能确实包含多个return语句。未能这样做很可能导致运行时问题,如资源泄漏、死锁等。

《Go语言之旅》并非对Golang的详尽介绍;它在演示defer在封闭函数正常返回时的工作方式方面做得很好,但遗漏了一些重要的微妙之处。

你对defer的理解有多深? ¶

测验时间! ¶

为了测试Gopher对defer语义的理解,我在Twitter上进行了以下投票:

函数foo和bar具有完全相同的行为/语义:

1
2
3
4
5
6
7
8
9
func foo(f func()) {
    defer fmt.Println("bye")
    f()
}

func bar(f func()) {
    f()
    fmt.Println("bye")
}

真还是假?

花点时间思考一下……你的答案会是什么?

尽管投票的响应率较低(只有16位受访者),且受访者的Golang熟练程度难以确定,但我确实发现结果很有启示性:大多数受访者(56%)回答“真”,但正确答案是“假”。

解释 ¶

函数foo和bar不具有相同的语义。具体来说,如果函数f在调用时发生panic——要么是因为它恰好为nil,要么是因为在其执行过程中发生panic——函数bar将直接panic(不打印任何内容),而函数foo将在panic之前向标准输出打印“bye”。

你可以在Go Playground上亲自尝试。在这种情况下,如果要无条件执行fmt.Println("bye"),使用defer不是可选的,因为函数bar无法保证其调用者传递的函数参数在调用时不会panic。

得益于defer提供的防panic保证,函数foo完全避免了这个问题。

用defer还是不用defer:这是个问题 ¶

这个例子应该作为一个警示。defer容易被误解(有罪!)为仅仅是语法便利。不使用它可能导致你的Go程序出现严重问题。

当然,如果你正在编写性能关键代码(如数据库)并且你知道自己在做什么,你可能不得不在代码的某些地方避免使用defer语句,但是

总是使用defer(除非你有充分的理由不这样做)。

是一个很好的经验法则。

引用Bill Kennedy(Twitter上的goinggodotnet)的劝诫,他在视频课程中经常重复:

正确性优先于性能。

牢固掌握defer的语义很可能为你的Go程序的正确性带来巨大回报。

不要推迟阅读关于defer的细则 :)

附加资源 ¶

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计