defer 关键字是 Golang 引入的特征关键字之一,方便开发者做开发,在某些场景真的是太方便了,可以少写很多重复的代码。比如在错误处理场景,如果有多次返回,资源清理工作可能就是重复的。
常规方式:
func FileProcess() (msg string, err error) { f, err := os.Open("demo-file-name") if err != nil { log.Fatalf("open file error, err: %#v", err) return } fi, err := f.Stat() // 获取 文件信息失败 if err != nil { log.Fatalf("get file stat fail, err: %#v", err) f.Close() return } // 如果长度小于 200 不处理 if fi.Size() < 200 { log.Fatalf("file size less than 200 bytes") f.Close() return } // Do something to deal with file content // .... return}
使用 defer:
func FileProcess() (msg string, err error) { f, err := os.Open("demo-file-name") if err != nil { log.Fatalf("open file error, err: %#v", err) return } defer f.Close() fi, err := f.Stat() // 获取 文件信息失败 if err != nil { log.Fatalf("get file stat fail, err: %#v", err) return } // 如果长度小于 200 不处理 if fi.Size() < 200 { log.Fatalf("file size less than 200 bytes") return } // Do something to deal with file content // .... return}
比较以上两种实现,使用 defer 的编码可以减少多次出现的 Close 方法,减少代码量,而且写出的代码更美观。说了这么多,来讲一下 defer 的作用和常见的使用场景。
1. defer 关键字作用:defer 关键字之后出现的表达式不会被立即执行,而是推迟到函数或者方法返回时执行,defer 会把执行的表达式和参数压入栈,等到外层函数返回时(return, 函数结束或者 panic)再把栈中的表达式弹出依次执行,遵循 LIFO 的顺序。如
func MyFunc() { defer statement1() defer statement2() defer statement3() return}
这段代码的 defer 入栈顺序是 statement1, statement2, statement3, 函数结束时的执行顺序是 statement3,statement2,statement1。
2. 使用场景。
2.1 清理资源。关闭打开的文件,断开网络连接等。
f, err := os.Open("demo-file-name") if err != nil { log.Fatalf("open file error, err: %#v", err) return }defer f.Close()
2.2 错误处理。处理 panic(注意,recover 必须在闭包中处理才有效)或者统一处理其他的错误。
func RecoverPanic() { defer func() { if err := recover(); err != nil { fmt.Println("recover from panic") } fmt.Println("RecoverPanic exit") }() fmt.Println("I will panic") panic("trigger recover")}
2.3 在返回时的业务规则统一处理。如 Restful 接口中, 可以使用 defer 处理最后的返回值赋值, 函数执行的 trace。
func TraceFunc() { trace.WithRegion(context.Background(), "TraceFunc", func() { fmt.Println("TraceFunc end") }) time.Sleep(time.Second * 5)}
3. defer 的执行时机。
defer 真正执行的时机是在 当前函数或者方法执行结束之后,但是执行权真正返还给调用者之前。func return |defer |Caller
defer 的坑
1. defer 语句执行时会把参数创建副本入栈(值传递)。
func DeferFunc() { var a int = 2 defer fmt.Printf("defer1 a: %d \n", a) a++ defer fmt.Printf("defer2 a: %d \n", a) a++}
执行结果不是下面这样
defer1 a: 4defer2 a: 4
Golang 的传参方式只有值传递,在执行表达式
`fmt.Printf("defer1 a: %d \n", a)
` 就把 变量 a 创建了 1 份拷贝压入栈中, 所以等到真正执行时取出的数据就是 2。
真正的执行结果如下:
2. 返回值处理。
在具名返回函数或者方法中,可以借助于 defer 在闭包中修改返回值。非具名返回值和非闭包函数均不能实现这一点, golang 中 return 之后出现的变量与其他函数变量完全一样,可以被闭包访问、修改。func NamedReturnFunc() (val1 int32, val2 int64) { defer func() { val1 = 5 }() val1 = 99 val2 = time.Now().Unix() fmt.Printf("NamedReturnFunc will return, val1: %d, val2: %d \n", val1, val2)return}
执行结果:
如果上面的代码改成下面这样,执行结果就会完全不同。
func NonNamedReturnFunc() (int32, int64) { var ( val1 int32 val2 int64 ) defer func() { val1 = 5 }() val1 = 99 val2 = time.Now().Unix() fmt.Printf("NonNamedReturnFunc will return, val1: %d, val2: %d \n", val1, val2) return val1, val2}
执行结果
3. defer 调用方法时,receiver 如果为 nil, 不会直接 panic, 一直到执行到才会 panic。
type MyType struct { Name string}func (t *MyType) Hello() { fmt.Printf("hello %s", t.Name)}func deferMethod() { var m *MyType defer m.Hello() fmt.Println("I will return")}
执行结果
今天介绍的 golang 的 defer 关键字使用场景和常见的坑, 你都 get 到了吗?
defer 用于推迟表达式的执行时机到当前函数结束。
defer 最常见的使用场景是 资源清理,错误处理(panic 恢复), 函数执行时间统计, 业务场景统一处理。
defer 在调用非闭包表达式时会把参数压入栈中, 之后的修改都不会影响到已经入栈的参数。
defer 后面如果调用方法,如果 receiver 是 nil,一直到函数终止后真正执行表达式时才会 panic。