defer是Go语言提供的一种用于延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行。

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer fmt.Println("ByeBye")
  5. fmt.Println("Hello")
  6. }

程序执行结果

  1. Hello
  2. ByeBye

defer语句通常用于一些成对操作的场景:打开连接/关闭连接;加锁/释放锁;打开文件/关闭文件等。一般采用如下模式

  1. package main
  2. import (
  3. "log"
  4. "os"
  5. )
  6. func main() {
  7. file, e := os.Open("test.txt")
  8. if e!=nil {
  9. log.Fatal(e.Error())
  10. }
  11. defer file.Close()
  12. }

例如创建一个文件复制函数

  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. dst, err := os.Create(dstName)
  7. if err != nil {
  8. return
  9. }
  10. written, err = io.Copy(dst, src)
  11. dst.Close()
  12. src.Close()
  13. return
  14. }

但是一旦文件os.create 函数执行错误 ,会导致源文件没有执行关闭操作

  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. defer src.Close()
  7. dst, err := os.Create(dstName)
  8. if err != nil {
  9. return
  10. }
  11. defer dst.Close()
  12. return io.Copy(dst, src)
  13. }

defer执行顺序

如果有多个defer函数,调用顺序类似于栈,越后面的defer函数越先被执行(后进先出)
  1. package main
  2. import "fmt"
  3. func main() {
  4. defer fmt.Println("1")
  5. defer fmt.Println("2")
  6. defer fmt.Println("3")
  7. }

执行结果

  1. 3
  2. 2
  3. 1

外部变量引用

defer函数对外部变量的引用是有两种方式的。分别是:
1函数参数,则在defer定义时就把值传递给defer,并被cache起来;
2闭包,则会在defer函数真正调用时根据整个上下文确定当前的值。

retrun

官方关于defer的说明

Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.

defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!
**函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。
defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。
其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:

  1. 返回值 = xxx
  2. 调用defer函数
  3. 空的return
  1. package main
  2. import "fmt"
  3. func f() (result int) {
  4. defer func() {
  5. result++
  6. }()
  7. return 0
  8. }
  9. func main() {
  10. r := f()
  11. fmt.Println(r)
  12. }

执行结果是1,将执行语句改成如下形式

  1. 它可以改写成这样:
  2. func f() (result int) {
  3. result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令
  4. func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
  5. result++
  6. }()
  7. return
  8. }

再看下面执行函数

  1. package main
  2. import "fmt"
  3. func f() (r int) {
  4. t := 5
  5. defer func() {
  6. t = t + 5
  7. }()
  8. return t
  9. }
  10. func main() {
  11. r := f()
  12. fmt.Println(r)
  13. }

执行结果是5
上面函数可以转换成如下

  1. func f() (r int) {
  2. t := 5
  3. r = t //赋值指令
  4. func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
  5. t = t + 5
  6. }
  7. return //空的return指令
  8. }
  9. 所以这个的结果是5
  1. package main
  2. import "fmt"
  3. func f() (r int) {
  4. defer func(r int) {
  5. r = r + 5
  6. }(r)
  7. return 1
  8. }
  9. func main() {
  10. r := f()
  11. fmt.Println(r)
  12. }

它改写后变成:

  1. func f() (r int) {
  2. r = 1 //给返回值赋值
  3. func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
  4. r = r + 5
  5. }(r)
  6. return //空的return
  7. }

所以这个例子的结果是1。

defer的实现

defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。
在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。
普通的函数返回时,汇编代码类似:

  1. add xx SP
  2. return

如果其中包含了defer语句,则汇编代码是:

  1. call runtime.deferreturn
  2. add xx SP
  3. return

从上面可以看出:

  1. 编译器会将defer声明编译为runtime.deferproc(fn),这样运行时,会调用runtime.deferproc,在deferproc中将所有defer挂到goroutine的defer链上;
  2. 编译器会在函数return之前(注意,是return之前,而不是return xxx之前,后者不是一条原子指令),增加runtime.deferreturn调用;这样运行时,开始处理前面挂在defer链上的所有defer。

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

  1. package main
  2. import "fmt"
  3. func foo() {
  4. i := 1
  5. defer fmt.Println(i)
  6. i++
  7. return
  8. }
  9. func main() {
  10. foo()
  11. }

上面程序执行结果是1 而不是2

panic&recover

  1. func panic(v interface{})

中英文说明 The panic built-in function stops normal execution of the current goroutine.When a function F calls panic, normal execution of F stops immediately.Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic,terminating G’s execution and running any deferred functions.This continues until all functions in the executing goroutine have stopped,in reverse order. At that point, the program is terminated and the error condition is reported,including the value of the argument to panic. This termination sequence is called panicking and can be controlled by the built-in function recover. panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获。

  1. package main
  2. import "fmt"
  3. func Bar() {
  4. defer func() {
  5. fmt.Println("c")
  6. }()
  7. Foo()
  8. fmt.Println("继续执行")
  9. }
  10. func Foo() {
  11. panic("异常执行a")
  12. }
  13. func main(){
  14. Bar()
  15. }

上面程序执行结果

  1. go run panic.go
  2. c
  3. panic: 异常执行a
  4. goroutine 1 [running]:
  1. func recover() interface{}

中英文说明 The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, or if the argument supplied to panic was nil, recover returns nil. Thus the return value from recover reports whether the goroutine is panicking. recover内置函数用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误如果goroutine中没有panic或者捕获的panic的值为nil,recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为

  1. package main
  2. import "fmt"
  3. func Bar() {
  4. defer func() {
  5. fmt.Println("c")
  6. }()
  7. Foo()
  8. fmt.Println("继续执行d")
  9. }
  10. func Foo() {
  11. defer func() {
  12. if err :=recover();err!=nil{
  13. fmt.Println(err)
  14. }
  15. fmt.Println("b")
  16. }()
  17. panic("异常执行a")
  18. }
  19. func main(){
  20. Bar()
  21. }

执行结果

  1. 异常执行a
  2. b
  3. 继续执行d
  4. c

在子协程里触发 panic,只能触发自己协程内的 defer,而不会调用 main 里的defer函数的。

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer func() {
  5. if err := recover(); err != nil {
  6. fmt.Println(err)
  7. }
  8. }()
  9. go func() {
  10. panic("go routine panic")
  11. }()
  12. fmt.Println("nothing happened")
  13. }

执行程序结果还是会出现painc

闭包

闭包会应用变量值

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. a := bar()
  7. fmt.Printf("a1=%d\n",a(1))
  8. fmt.Printf("a2=%d\n",a(1))
  9. fmt.Printf("a3=%d\n",a(1))
  10. b := bar()
  11. fmt.Printf("b1=%d\n",b(1))
  12. fmt.Printf("b2=%d\n",b(1))
  13. fmt.Printf("b3=%d\n",b(1))
  14. }
  15. func bar()func(int)int{
  16. var i int
  17. return func(j int) int {
  18. fmt.Printf("point=%v i=%d j=%d\n",&i,i,j)
  19. i+=j
  20. return i
  21. }
  22. }

打印结果

  1. point=0xc000014088 i=0 j=1
  2. a1=1
  3. point=0xc000014088 i=1 j=1
  4. a2=2
  5. point=0xc000014088 i=2 j=1
  6. a3=3
  7. point=0xc0000140d8 i=0 j=1
  8. b1=1
  9. point=0xc0000140d8 i=1 j=1
  10. b2=2
  11. point=0xc0000140d8 i=2 j=1
  12. b3=3

参考

https://segmentfault.com/a/1190000018169295
https://juejin.im/post/5dd3bd2051882521ca67a6bd
https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01