延迟语句(defer)是Go 语言里一个非常有用的关键字,它能把资源的释放语句与申请语句放到距离相近的位置,从而减少了资源泄漏的情况发生。
延迟语句是什么
defer 是Go 语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return 正常结束或者panic 导致的异常结束)执行。在需要释放资源的场景非常有
用,可以很方便地在函数结束前做一些清理操作。在打开资源语句的下一行,直接使用defer 就可
以在函数返回前释放资源,可谓相当有效。
defer 通常用于一些成对操作的场景:打开连接/关闭连接、加锁/释放锁、打开文件/关闭文件
等。使用非常简单:
f,err := os.Open(filename)if err != nil {panic(err)}if f != nil {defer f.Close()}
在打开文件的语句附近,用defer 语句关闭文件。这样,在函数结束之前,会自动执行defer
后面的语句来关闭文件。注意,要先判断f 是否为空,如果f 不为空,再调用f.Close()函数,避免出
现异常情况。
当然,defer 会有短暂延迟,对时间要求特别高的程序,可以避免使用它,其他情况一般可以忽略它带来的延迟。特别是Go 1.14 又对defer 做了很大幅度的优化,效率提升了不少。
我们举一个反面例子:
r.mu.Lock()
rand.Intn(param)
r.mu.Unlock()
上面只有三行代码,看起来这里不用 defer 执行 Unlock 并没有什么问题。其实并不是这样,
中间这行代码rand.Intn(param)其实是有可能发生 panic 的,更严重的情况是,这段代码很有可能被其他人修改,增加更多的逻辑,而这完全不可控。也就是说,在 Lock 和 Unlock 之间的代码一旦出现异常情况导致 panic,就会形成死锁。因此这里的逻辑是,即使是看起来非常简单的代码,使用 defer 也是有必要的,因为需求总是在变化,代码也总会被修改。
延迟语句的执行顺序是什么
每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被复制下来;当外层函数(注意不是代码块,如一个 for 循环块并不是外层函数)退出时,defer 函数按照定义的顺序逆序执行;如果 defer 执行的函数为nil,那么会在最终调用函数的时候产生 panic
defer 语句并不会马上执行,而是会进入一个栈,函数return 前,会按先进后出的顺序执行。也就是说,最先被定义的defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行;否则,如果前面先执行了,那后面函数的依赖就没有了,因而可能会出错。
在 defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用。前者在 defer 定
义时就把值传递给 defer,并被 cache 起来;后者则会在 defer 函数真正调用时根据整个上下文确
定参数当前的值。
defer 后面的函数在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。真正执
行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个“值”,那么就和定义的时候
是一致的。如果此变量是一个“引用”,那就可能和定义的时候不一致。
举个例子:
func main() {var whatever [3]struct{}for i := range whatever {defer func() {fmt.Println(i)}()}
}
执行结果:
2
1
0
defer 后面跟的是一个闭包(后面小节会讲到),i 是“引用”类型的变量,for 循环结束后 i的值为 2,因此最后打印了2 1 0。
有了上面的基础,再来看一个例子:
type number intfunc (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {var n numberdefer n.print()defer n.pprint()defer func() { n.print() }()defer func() { n.pprint() }()n = 3
}
执行结果:
3
3
3
0
注意,defer 语句的执行顺序和定义的顺序相反。
第四个 defer 语句是闭包,引用外部函数的 n,最终结果是 3;第三个 defer 语句同上,也是闭包;第二个 defer 语句,n 是引用,最终求值是 3; 第一个 defer 语句,对 n 直接求值,开始的时候 n=0,所以最后是 0。
我们再来看两个延伸情况。例如,下面的例子中,return 之后的 defer 语句会执行吗?
func main() {defer func() {fmt.Println("before return")}()if true {fmt.Println("during return")return}defer func() {fmt.Println("after return")}()
}
运行结果:
during return
before return
解析:return 之后的 defer 函数不能被注册,因此不能打印出 after return。
第二个延伸示例则可以视为对defer 的原理的利用。某些情况下,会故意用到 defer 的“先求
值,再延迟调用”的性质。想象这样的场景:在一个函数里,需要打开两个文件进行合并操作,合
并完成后,在函数结束前关闭打开的文件句柄。
func mergeFile() error {// 打开文件一f, _ := os.Open("file1.txt")if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close file1.txt err %v\n", err)}}(f)}// 打开文件二f, _ = os.Open("file2.txt")if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close file2.txt err %v\n", err)}}(f)}// ……return nil
}
上面的代码中就用到了 defer 的原理,defer 函数定义的时候,参数就已经复制进去了,之
后,真正执行 close() 函数的时候就刚好关闭的是正确的“文件”了,很巧妙。如果不这样,将 f
当成函数参数传递进去的话,最后两个语句关闭的就是同一个文件了:都是最后一个打开的文件。
在调用 close() 函数的时候,要注意一点:先判断调用主体是否为空,否则可能会解引用了一
个空指针,进而 panic。
如何拆解延迟语句
如果 defer 像前面介绍的那样简单,这个世界就完美了。但事情总是没这么简单,defer 用得
不好,会陷入泥潭。
避免陷入泥潭的关键是必须深刻理解下面这条语句:
return xxx
上面这条语句经过编译之后,实际上生成了三条指令:
1)设置返回值 = xxx。
2)调用 defer 函数。
3)空的 return。讲返回值返回
第 1 和第3 步是 return 语句生成的指令,也就是说return 并不是一条原子指令;第 2 步是
defer 定义的语句,这里可能会操作返回值,从而影响最终结果。
下面来看两个例子,试着将 return 语句和 defer 语句拆解到正确的顺序。
第一个例子:
func f() (res int) {t := 5defer func() {t = t + 5}()return t
}
拆解后:
func f() (res int) {t := 5// 1. 赋值指令res = t// 2. defer 被插入到赋值与返回之间执行,这个例子中返回值 res 没被修改过func() {t = t + 5}// 3. 空的return 指令return
}
这里第二步实际上并没有操作返回值 r,因此,main 函数中调用 f() 得到 5。
第二个例子:
func f() (res int) {defer func(res int) {res = res + 5}(res)return 1
}
拆解后:
func f() (res int) {// 1. 赋值res = 1// 2. 这里改的 res 是之前传进去的 res,不会改变要返回的那个 res值func(res int) {res = res + 5}(res)// 3. 空的returnreturn
}
第二步,改变的是传值进去的 r,是形参的一个复制值,不会影响实参 r。因此,main 函数中
需要调用f()得到1。
第三个例子:
package mainimport "fmt"func main() {res := deferRun()fmt.Println(res)
}func deferRun() (res int) {num := 1 defer func() {res++}() return num
}
运行结果:
2
在本例中,第一步是将result
的值设置为num
,此时还未执行defer
,num
的值是1
,所以result
被设置为1
,然后再执行defer
语句将result+1
,最终将result
返回,所以会打印出 2
。
如果把defer中的res++改成num++
func deferRun() (res int) {num := 1defer func() {num++}() return num
}
运行结果:
1
第一步是将result
的值设置为num
,此时还未执行defer
,num
的值是1
,所以result
被设置为1
,然后再执行defer 即num+1
,要返回的result
并没有变,
最终将result
返回,所以会打印出 1
。
如何确定延迟语句的参数
defer 语句表达式的值在定义时就已经确定了。下面通过三个不同的函数来理解:
func f1() {var err errordefer fmt.Println(err)err = errors.New("defer1 error")return
}
func f2() {var err errordefer func() {fmt.Println(err)}()err = errors.New("defer2 error")return
}
func f3() {var err errordefer func(err error) {fmt.Println(err)}(err)err = errors.New("defer3 error")return
}
func main() {f1()f2()f3()
}
运行结果:
<nil>
defer2 error
<nil>
第 1 和第3 个函数中,因为作为参数,err 在函数定义的时候就会求值,并且定义的时候 err
的值都是 nil,所以最后打印的结果都是 nil;第 2 个函数的参数其实也会在定义的时候求值,但
第 2 个例子中是一个闭包,它引用的变量 err 在执行的时候值最终变成 defer2 error 了。
func deferrun3() {num := 1defer func() {fmt.Println(num)}()num++return
}
运行结果: 原理同上述第二个例子,也是闭包
2
现实中第 3 个函数比较容易犯错误,在生产环境中,很容易写出这样的错误代码,导致最后
defer 语句没有起到作用,造成一些线上事故,要特别注意。
闭包是什么
闭包是由函数及其相关引用环境组合而成的实体,即:闭包=函数+引用环境。
一般的函数都有函数名,而匿名函数没有。匿名函数不能独立存在,但可以直接调用或者赋值于某个变量。匿名函数也被称为闭包,一个闭包继承了函数声明时的作用域。在 Go 语言中,所有的匿名函数都是闭包。
有个不太恰当的例子:可以把闭包看成是一个类,一个闭包函数调用就是实例化一个类。闭包在运行时可以有多个实例,它会将同一个作用域里的变量和常量捕获下来,无论闭包在什么地方被调用(实例化)时,都可以使用这些变量和常量。而且,闭包捕获的变量和常量是引用传递,不是值传递。
举个简单的例子:
func main() {var a = Accumulator()fmt.Printf("%d\n", a(1))fmt.Printf("%d\n", a(10))fmt.Printf("%d\n", a(100))fmt.Println("------------------------")var b = Accumulator()fmt.Printf("%d\n", b(1))fmt.Printf("%d\n", b(10))fmt.Printf("%d\n", b(100))
}
func Accumulator() func(int) int {var x intreturn func(delta int) int {fmt.Printf("(%+v, %+v) - ", &x, x)x += deltareturn x}
}
执行结果是:
(0xc420014070, 0) - 1
(0xc420014070, 1) - 11
(0xc420014070, 11) - 111
------------------------
(0xc4200140b8, 0) - 1
(0xc4200140b8, 1) - 11
(0xc4200140b8, 11) – 111
闭包引用了 x 变量,a,b 可看作 2 个不同的实例,实例之间互不影响。实例内部,x 变量
是同一个地址,因此具有“累加效应”。
延迟语句如何配合恢复语句
Go 语言被诟病多次的就是它的 error,实际项目里经常出现各种 error 满天飞,正常的代码逻
辑里有很多 error 处理的代码块。函数总是会返回一个 error,留给调用者处理;而如果是致命的错
误,比如程序执行初始化的时候出问题,最好直接 panic 掉,避免上线运行后出更大的问题。
有些时候,需要从异常中恢复。比如服务器程序遇到严重问题,产生了 panic,这时至少可以
在程序崩溃前做一些“扫尾工作”,比如关闭客户端的连接,防止客户端一直等待等;并且单个请求导致的 panic,也不应该影响整个服务器程序的运行。
recover异常捕获
异常其实就是指程序运行过程中发生了panic
,那么我们为了不让程序报错退出,可以在程序中加入recover
机制,将异常捕获,打印出异常,这样也方便我们定位错误。而捕获的方式我们之前在讲defer
的时候也提到过,一般是用recover
和defer
搭配使用来捕获异常。
下面请看个具体例子:
func main() { defer func() { if error:=recover();error!=nil{ fmt.Println("出现了panic,使用reover获取信息:",error) } }() fmt.Println("11111111111") panic("出现panic") fmt.Println("22222222222") }
运行结果:
11111111111
出现了panic,使用reover获取信息: 出现panic
注意,这里有了recover
之后,程序不会在panic
出中断,再执行完panic
之后,会接下来执行defer recover
函数,但是当前函数panic
后面的代码不会被执行,但是调用该函数的代码会接着执行。
如果我们在main
函数中未加入defer func(){...}
,当我们的程序运行到底8行时就会panic
掉,而通常在我们的业务程序中对于程序panic
是不可容忍的,我们需要程序健壮的运行,而不是是不是因为一些panic
挂掉又被拉起,所以当发生panic
的时候我们要让程序能够继续运行,并且获取到发生panic
的具体错误,这就可以用上述方法。
panic传递
当一个函数发生了panic
之后,若在当前函数中没有recover
,会一直向外层传递直到主函数,如果迟迟没有recover
的话,那么程序将终止。如果在过程中遇到了最近的recover
,则将被捕获。
看下面例子:
package mainimport "fmt"func testPanic1(){fmt.Println("testPanic1上半部分")testPanic2()fmt.Println("testPanic1下半部分")
}func testPanic2(){defer func() {recover()}()fmt.Println("testPanic2上半部分")testPanic3()fmt.Println("testPanic2下半部分")
}func testPanic3(){fmt.Println("testPanic3上半部分")panic("在testPanic3出现了panic")fmt.Println("testPanic3下半部分")
}func main() {fmt.Println("程序开始")testPanic1()fmt.Println("程序结束")
}
运行结果:
程序开始
testPanic1上半部分
testPanic2上半部分
testPanic3上半部分
testPanic1下半部分
程序结束
解析:
调用链:main-->testPanic1-->testPanic2-->testPanic3
,但是在testPanic3
中发现了一个panic
,由于testPanic3
没有recover
,向上找,在testPanic2
中找到了recover
,panic
被捕获了,程序接着运行,由于testPanic3
发生了panic
,所以不再继续运行,函数跳出返回到testPanic2
,testPanic2
中捕获到了panic
,也不会再继续执行,跳出函数testPanic2
,到了testPanic1
接着运行。
所以recover
和panic
可以总结为以下两点:
这里的调用链指的是同一个函数中(如果panic是在另外一个go程中,是捕获不到的。即一个go程是无法捕获到另一个go程中的panic)
recover()
只能恢复当前函数级或以当前函数为首的调用链中的函数中的panic()
,恢复后调用当前函数结束,但是调用此函数的函数继续执行- 函数发生了
panic
之后会一直向上传递,如果直至main
函数都没有recover()
,程序将终止,如果是碰见了recover()
,将被recover
捕获。
defer...recover
panic 会停掉当前正在执行的程序,而不只是当前线程。在这之前,它会有序地执行完当前线
程 defer 列表里的语句,其他协程里定义的 defer 语句不作保证。所以在 defer 里定义一个recover 语句,防止程序直接挂掉,就可以起到类似 Java 里 try...catch 的效果。
注意,recover() 函数只在 defer 的函数中直接调用才有效。例如:
func main() {defer fmt.Println("defer main")var user = os.Getenv("USER_")go func() {defer func() {fmt.Println("defer caller")if err := recover(); err != nil {fmt.Println("recover success. err: ", err)}}()func() {defer func() {fmt.Println("defer here")}()if user == "" {panic("should set user env.")}// 此处不会执行fmt.Println("after panic")}()}()time.Sleep(100)fmt.Println("end of main function")
}
程序的执行结果:
defer here
defer caller
recover success. err: should set user env.
end of main function
defer main
代码中的 panic 最终会被 recover 捕获到。这样的处理方式在一个 http server 的主流程常常
会被用到。一次偶然的请求可能会触发某个 bug,这时用 recover 捕获 panic,稳住主流程,不影
响其他请求。
同样,我们再来看几个延伸示例。这些例子都与 recover() 函数的调用位置有关。
考虑以下写法,程序是否能正确 recover 吗?如果不能,原因是什么:
func main() {defer f()panic(404)
}
func f() {if e := recover(); e != nil {fmt.Println("recover")return}
}
能。在 defer 的函数中调用,生效。
func main() {recover()panic(404)
}
不能。直接调用 recover,返回 nil。
func main() {defer recover()panic(404)
}
不能。要在 defer 函数里调用 recover。
func main() {defer func() {if e := recover(); e != nil {fmt.Println("recover")}}()panic(404)
}
能。在 defer 的函数中调用,生效。
func main() {defer func() {recover()}()panic(404)
}
能。在 defer 的函数中调用,生效。
func main() {defer func() {defer func() {recover()}()}()panic(404)
}
不能。多重 defer 嵌套。
为什么无法从父goroutine 恢复子goroutine 的panic
对于这个问题,其实更普遍问题是:为什么无法 recover 其他 goroutine 里产生的 panic?
为什么不能从父 goroutine 中恢复子 goroutine 的 panic?或者一般地说,为什么某个
goroutine 不能捕获其他 goroutine 内产生的 panic?
是设计使然:因为goroutine 被设计为一个独立的代码执行单元,拥有自己的执行栈,不与其他 goroutine 共享任何数据。这意味着,无法让 goroutine 拥有返回值、也无法让 goroutine 拥有自身的 ID 编号等。若需要与其他 goroutine 产生交互,要么可以使用 channel 的方式与其他 goroutine 进行通信,要么通过共享内存同步方式对共享的内存添加读写锁。