文章目录
Golang 中的 defer 关键字允许我们在函数退出的时候执行一些资源回收操作,比如关闭数据库连接、关闭文件等。常规使用 defer 时一般没什么问题,但是遇到 corner case 的时候总是有点搞不清,这里记录一个笔记。
happy case
正常使用 defer 的情况如下,比如函数打开一个文件,在函数返回的时候,关闭这个文件,否则会有文件泄漏 too many open files
的问题。
func foo() error {
f, err := os.Open("/home/test")
if err != nil {
return err
}
defer func(){
f.Close()
}
// ...
return nil
}
引用外部变量
引用外部变量的方式有两种,一种情况是函数参数,另一种情况是闭包引用。前者在 defer 定义的时候就把值传递给 defer,并被 cache 起来;后者则在 defer 函数中真正调用时根据上下文确定当前的值。
函数参数
函数参数也是值传递,如果是值类型,则是一个完全拷贝,如果是引用类型,则类似于一个指针,分别看下面例子。注意在第二个例子中,如果我们使用 a = append(a, "a")
重新赋值 a,这样对 defer 中的参数是没有影响的,因为对 append 是对 SliceHeader 的一个拷贝,结构体本身是值类型,但是结构体有一个数值指针。因此需要记住 append
对 slice 的修改不是原地的,在需要追加操作时,需要返回一个新的 slice。
func paramValue() {
a := 1
defer func(p int) {
fmt.Println("value p:", p) // 打印:value p: 1
}(a)
a = 34
}
func paramRef() {
a := make([]string, 2)
defer func(p []string) {
fmt.Println("ref p:", p) // 打印 ref p: [a b]
}(a)
a[0] = "a"
a[1] = "b"
}
闭包引用
闭包引用,在实际调用函数时,会根据上下文来决定引用变量的值。如在下面的例子中,将打印 2。在 Golang 中,闭包是一个匿名函数以及该匿名函数引用的函数外的变量的整体,即闭包等于:匿名函数 + 引用的外部变量。在执行闭包函数的时候,引用的外部变量的值即是在调用时刻该外部变量的值。在下面例子中,闭包引用了外部变量 i,在 defer 声明的时候,i 的值为 0,但在执行的时候,其值是 2.
func main() {
closure()
}
func closure() {
i := 0
defer func() {
fmt.Println(i) // 打印 2
}()
i = 2
}
defer/recover 捕获 panic
在 golang 中,如果一个 goroutine 发生了 panic,那么整个程序将会退出。为了避免这种情况,golang 支持使用 recover 方法捕获 goroutine 发生的 panic,并做一些善后处理。recover 方法只能在 defer 中调用才有效。另外 golang 中 panic 方法的参数和 recover 返回值都是 any 类型(interface{})。
func deferRecover() {
defer func() {
if a := recover(); a != nil {
fmt.Println("catch a panic:", a)
// 打印:catch a panic: [some thing unexpected]
}
}()
subFunc()
}
func subFunc() {
panic([]string{"some thing unexpected"})
}
golang 的 recover 只能捕获同一个 goroutine 发生的 panic,对其他 goroutine 发生的 panic 无法捕获。并且在 panic 发生之后也无法捕获,比如下面的例子无法捕获。
func parent() {
panic("panic")
deferRecover()
}
func deferRecover() {
defer func() {
if a := recover(); a != nil {
fmt.Println("catch a panic:", a)
}
}()
}
修改函数返回值
这里主要是理解 golang return
关键字不是原子的,具体是有三条指令:1)返回值 = xxx;2)调用 defer 函数;3)空的 return。第 1)步和第 3)步是 return 语句生成的指令。
修改函数返回值具体又分命名返回值
和非命名返回值
两种情况,在命名返回值的情况中,不管返回值是值类型还是引用类型,都可以在 defer 中直接修改。在非命名返回值的情况,只有引用类型才可以在 defer 中进行修改。不过为了避免歧义,当需要在 defer 中修改返回值时,可以总是将参数定义为命名返回值。
非命名返回值
参考下面两个例子: modifyReturn 以及 modifyReturnPointer,其中前者修改了一个返回值 string,因为 string 是值类型,很明显 defer 中的修改是无效的。后者修改了一个指针,因此是起作用的。
func main() {
fmt.Println(modifyReturn()) // 打印 "a string"
fmt.Println(*modifyReturnPointer()) // 打印 "another"
}
func modifyReturn() string {
a := "a string"
defer func() {
a = "another"
}()
return a
}
func modifyReturnPointer() *string {
a := pointer("a string")
defer func() {
*a = "another"
}()
return a
}
func pointer[T any](a T) *T {
return &a
}
对于这个问题,还需要注意 golang 中, struct 是值类型,除非 struct 的字段包含了指针;interface 类型是引用类型,
命名返回值
命名返回值的情况比较简单,只要在 defer 修改了返回值变量,就会体现到返回值中。
func main() {
fmt.Println(modifyReturnParam()) // 打印 "another"
}
func modifyReturnParam() (s string) {
defer func() {
s = "another"
}()
return "origin"
}
执行顺序
golang 中的 defer 是先声明的后执行,跟 stack 是一样的。