// TODO 仔细阅读 Golang: 深入理解panic and recover

defer

一个 defer 调用的函数将被暂时保存到调用列表中. 保存的调用列表在当前环境返回的时候被执行. defer一般可以用于简化代码, 执行各种清理操作.

让我们演示一个文件复制的例子: 函数需要打开两个文件, 然后将其中一个文件的内容复制到另一个文件:

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

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}

	written, err = io.Copy(dst, src)
	dst.Close()
	src.Close()
	return
}

上面的代码虽然能够工作, 但是隐藏一个bug. 如果第二个os.Open调用失败, 那么会在没有释放 source文件资源的情况下返回. 虽然我们可以通过在第二个返回语句前添加src.Close()调用 来修复这个bug; 但是当代码变得复杂时, 类似bug将很难被发现和修复. 通过defer语句, 我们可以确保每个文件被关闭:

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

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

Defer语言可以让我们在打开文件时就思考如何关闭文件. 不管函数如何返回, 文件关闭语句始终会被执行.

Defer语句的行为简单且可预测. 有三个基本原则:

  1. 参数在声明时获取
  2. 后进先出的顺序执行
  3. 可在return语句执行后读取或修改命名的返回值

参数值

defer函数的参数值,是在申明defer时确定下来的

在defer函数申明时,对外部变量的引用是有两种方式:

  • 函数参数: 在defer申明时就把值传递给defer,并将值缓存起来,调用defer的时候使用缓存的值进行计算
  • 闭包引用: 在defer函数执行时根据整个上下文确定当前的值

在这个例子中, 表达式i的值将在defer fmt.Println(i)时被计算. defer将会在 当前函数返回的时候打印 0.

1
2
3
4
5
6
func a() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}
1
2
3
4
5
6
7
8
9
10
11
func add(a, b int) int {
    return a + b
}

func test() {
    a, b := 1, 2
    defer fmt.Printf("defer is %d", add(a, b))
    a = 4
    b = 4
    fmt.Printf("test result is %d, %d ", a, b)
}

上例中,在defer函数被声明时,defer函数的参数就会被计算。此处的defer函数是 fmt.Printf, 其参数为前面的string和后面的add(a,b)。所以此处实际上是在声明时将add(a,b)的结果直接计算出来3,然后作为参数。

1
2
3
4
5
6
7
8
9
10
11
func t() {
    for i:=0; i<3; i++ {
        defer fmt.Printf("defer param: ", i)
    }

    for i:=0; i<3; i++ {
        defer func() {
            fmt.Printf("closures param: ", i)
        }
    }
}

输出为如下,该例中第一个循环,i是作为defer函数的参数,所以在声明的时候就缓存下来了,输出的是声明时计算出的缓存值。

第二个循环中,i是在defer函数中作为闭包的引用,在执行时才根据上下文确定当前值。defer 执行的时候,循环已经执行完成,所以i在defer时是 3

1
2
3
4
5
6
closures param: 3
closures param: 3
closures param: 3
defer param: 2
defer param: 1
defer param: 0 

执行顺序

defer调用的函数将在当前函数返回的时候, 以后进先出的顺序执行.

下面的函数将输出3210:

1
2
3
4
5
func b() {
	for i := 0; i < 4; i++ {
		defer fmt.Print(i)
	}
}

执行时机

defer调用的函数可以在返回语句执行后读取或修改命名的返回值.

在这个例子中, defer语句将会在当前函数返回后将i增加1. 实际上, 函数会返回2:

1
2
3
4
func c() (i int) {
	defer func() { i++ }()
	return 1
}

defer 函数在return 语句之后,返回调用函数之前执行。利用该特性, 我们可以方便修改函数的错误返回值. 以后应该可以看到类似的例子.

例1:

1
2
3
4
5
6
func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}

例2:

1
2
3
4
5
6
7
func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

例3:

1
2
3
4
5
6
7
func f() (r *int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return *t
}

例4:

1
2
3
4
5
6
7
func f() (r int) {
    defer func(r int) {
          r = r + 5
          fmt.Printf("defer r is %d", r)
    }(r)
    return 1
}

解析:

例1 return语句中返回值为 0,即将result设置为0, 之后执行defer函数result++,result 变为 1. 最终调用函数获取到的返回值为 1

例2 返回变量为 r,通过return 语句将 r 设置为 t的值5. 之后在defer函数中修改t的值,但是此处修改已经与返回值无关,所以最终的返回值不变还是 5

例3 同例2,只是返回值为指针,因此,在defer函数中修改t,同时也修改了返回值 r, 所以返回值是 10

例4 首先 defer 函数的参数被计算,此处defer函数的参数为rint的默认值0.然后return语句设置返回值r1。最后defer函数执行,由于defer函数的参数是r,故在defer函数中执行的r=r+5实际上是defer函数的局部变量,而不是函数f()的返回值变量。输出为 defer r is 5. 同样,这个的修改也不会影响f()的返回值.最终返回值为 1.

下面的代码执行可能有什么问题?

1
2
3
4
5
6
7
8
9
10
11
func f(filenames []string) {
    for _, name := range filenames {
        f, err := os.Open(name)
        if err != nil {
            return err 
        }
        defer f.Close()

        ...
    }
}

上面的代码中循环打开文件并进行操作,在循环中设置了defer函数来关闭文件。但是由于defer函数需要在函数返回时才执行,所以会先打开所有文件并处理,f() 返回前才会统一的关闭文件,当文件列表过长时,可能会导致耗光文件描述符。一种解决方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func f(filenames []string) {
    for _, name := range filenames {
        t(name)
    }
}

func t(name string ) {
    f, err := os.Open(name)
    if err != nil {
        return err 
    }
    defer f.Close()

    ...
}

修改为上面之后,在t()执行完之后,文件就会关闭,即一个文件处理完就关闭该文件。这样避免了之前必然存在所有文件都处于打开状态的问题。

panic

panic是一个內建函数,用于停止执行常规控制流程,并开始安全退出。当一个函数F调用了panic后,F将停止执行,任何F中的被defer开始被正常执行,然后F将控制权返回给它的调用者。对于F的调用者来说,这时F的行为就像是一次panic调用一样。这个过程将沿着栈继续往上,直到当前goroutine中的所有函数都返回,此时程序将崩溃。Panics可以通过直接调用panic函数发起。它们也可能由运行时错误引起,比如数组访问越界等。

panic会停掉当前正在执行的程序(注意,不只是协程),但是与os.Exit(-1)这种直愣愣的退出不同,panic的撤退比较有秩序,他会先处理完当前goroutine已经defer挂上去的任务,执行完毕后再退出整个程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
	"os"
	"fmt"
	"time"
)

func main() {
	defer fmt.Println("defer main") // will this be printed when panic?
	var user = os.Getenv("USER_")
	go func() {
        defer fmt.Println("defer caller")
        func() {
            defer func() {
                fmt.Println("defer here")
            }()

            if user == "" {
                panic("should set user env.")
            }
        }()
	}()

    time.Sleep(1 * time.Second)
    fmt.Printf("get result")
}

go run一下:

1
2
defer here
defer caller

如上例,main中增加一个defer,但会睡1s,在这个过程中panic了,还没等到main睡醒,进程已经退出了,因此maindefer不会被调到;而跟panic同一个goroutinedefer caller还是会打印的,并且其打印在defer here之后。

如果把time.Sleep()去掉,会发现main中的get resultdefer main都调用了,但我觉得这并不是一定的,只是因为并发的存在,panic的同时main也在往下执行。

总结:

  • 遇到处理不了的错误,找panic
  • panic有操守,退出前会执行本goroutine的defer,方式是原路返回(reverse order)
  • panic不多管,不是本goroutine的defer,不执行

recover

recover也是一个內建函数,它用于重新获取一个panicking goroutine的控制权。recover只有在被defer的函数中才有用。在正常执行期间,调用recover将会返回nil,并且不会有任何影响。如果当前goroutine正在清场,则一次recover调用将能够捕获到panic的值,并恢复正常执行。

需要注意的问题

defer 定义的位置

defer 表达式的函数如果定义在 panic 后面,该函数在 panic 后就无法被执行到

在defer前panic

1
2
3
4
5
6
func main() {
    panic("a")
    defer func() {
        fmt.Println("b")
    }()
}

结果,b没有被打印出来:

1
2
3
4
5
6
panic: a

goroutine 1 [running]:
main.main()
    /xxxxx/src/xxx.go:50 +0x39
exit status 2

而在defer后panic

1
2
3
4
5
6
func main() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果,b被正常打印:

1
2
3
4
5
6
7
b
panic: a

goroutine 1 [running]:
main.main()
    /xxxxx/src/xxx.go:50 +0x39
exit status 2

panic 被捕获

F中出现panic时,F函数会立刻终止,不会执行F函数内panic后面的内容,但不会立刻return,而是调用F的defer,如果F的defer中有recover捕获,则F在执行完defer后正常返回,调用函数F的函数G继续正常执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("b")
    }()
    panic("a")
}

结果:

1
2
3
4
捕获异常: a
b
继续执行
c

panic 未捕获

如果F的defer中无recover捕获,则将panic抛到G中,G函数会立刻终止,不会执行G函数内后面的内容,但不会立刻return,而调用G的defer…以此类推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func G() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果:

1
2
3
b
捕获异常: a
c

panic 的范围

如果一直没有recover,抛出的panic到当前goroutine最上层函数时,程序直接异常终止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("继续执行")
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    panic("a")
}

结果:

1
2
3
4
5
6
7
8
9
10
b
c
panic: a

goroutine 1 [running]:
main.F()
    /xxxxx/src/xxx.go:61 +0x55
main.G()
    /xxxxx/src/xxx.go:53 +0x42
exit status 2

recover 的范围

recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成程序崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func G() {
    defer func() {
        //goroutine外进行recover
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
        fmt.Println("c")
    }()
    //创建goroutine调用F函数
    go F()
    time.Sleep(time.Second)
}

func F() {
    defer func() {
        fmt.Println("b")
    }()
    //goroutine内部抛出panic
    panic("a")
}

结果:

1
2
3
4
5
6
7
8
9
b
panic: a

goroutine 5 [running]:
main.F()
    /xxxxx/src/xxx.go:67 +0x55
created by main.main
    /xxxxx/src/xxx.go:58 +0x51
exit status 2

recover 返回值

recover返回的是interface{}类型而不是go中的 error 类型,如果外层函数需要调用err.Error(),会编译错误,也可能会在执行时panic

1
2
3
4
5
6
7
8
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", err.Error())
        }
    }()
    panic("a")
}

编译错误,结果:

1
err.Error undefined (type interface {} is interface with no methods)
1
2
3
4
5
6
7
8
func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕获异常:", fmt.Errorf("%v", err).Error())
        }
    }()
    panic("a")
}

结果:

1
捕获异常: a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import(
	"fmt"
	"reflect"
)

func main()  {
    defer func() {
        if err:=recover();err!=nil{
            f:=err.(func()string)
            fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())
        }
    }()
    panic(func() string {
            return  "main panic"
        })
}

执行

1
0x47d9c0 main panic func

多个panic

1
2
3
4
5
6
7
package main
func main() {
	defer func() {
		panic("defer panic")
	}()
	panic("main panic")
}

运行

1
2
3
panic: main panic
        panic: defer panic
...省略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import(
	"fmt"
)

func main()  {
	test()
	fmt.Println("main end")
}

func test() {
	defer func() {
        if err:=recover();err!=nil{
            fmt.Println(err)
        }
    }()

    defer func() {
        panic("defer panic")
    }()
    panic("test panic")
}

执行

1
2
defer panic
main end

当recover前有多个panic时, 捕获到的只有最后一个.且捕获后,程序不会退出,而是正常运行。

参考文献