Go defer陷阱[译]

对等于nil的函数defer操作

如果通过defer调用一个值等于nil的函数,会引发panic错误。

1
2
3
4
5
func main() {
var run func() = nil
defer run()
fmt.Println("runs")
}

输出:

1
2
3
runs
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x452dc8]

原因:
在函数进行到最后之后,才执行run()函数,并引发panic错误,因为这是一个值为nil的函数


for循环中调用defer

image

上面的defer row.Close()for循环中并不会立即执行直到函数结束(第一次循环结束并不会执行)。在调用defer会产生一个函数调用栈,如果循环次数过多将会产生意料之外的问题。

解决方法:

1.直接调用row.Close()不使用defer
1
2
3
4
5
6
7
8
9
func ()  {
for {
row, err := db.Query("SELECT ...")
if err != nil{
...
}
row.Close()
}
}
2.将操作放入一个匿名函数中,当函数结束时将会执行defer调用的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ()  {
for {
func() {
row, err := db.Query("SELECT ...")
if err != nil {
..
}
defer row.Close()
..
// deferred funcs run here
}()

}
}

Defer as wrapper

有些时个你需要对闭包使用defer以追求实用或者有其它的一些原因。比如:要打开数据库连接,然后运行一些查询,最后运行以确保断开连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
type database struct{}
func (db *database) connect() (disconnect func()) {
fmt.Println("connect")
return func() {
fmt.Println("disconnect")
}
}

func main() {
db := &database{}
defer db.connect()
fmt.Println("query db...")
}

输出:

1
2
query db...
connect

为什么最后没有执行disconnect?这里出现这个bug的原因是connect函数返回的值并没有执行而是被保存了起来。

解决方法:

1
2
3
4
5
6
7
func main() {
db := &database{}
close := db.connect()
defer close()

fmt.Println("query db...")
}

不好的实践:
虽然下面的代码可以正常的执行,但是不推荐这样使用。

1
2
3
db := &database{}
defer db.connect()()
..


 Defer in a block

你可能会想deferred func会在结束一个代码块的时候执行,实际上deferred func只会在包含它的函数结束的时候执行。

1
2
3
4
5
6
7
8
9
func main() {
{
defer func() {
fmt.Println("block: defer runs")
}()
fmt.Println("block: ends")
}
fmt.Println("main: ends")
}

输出:

1
2
3
block: ends
main: ends
block: defer runs

原因:
deferred func只会在函数代码块结束的时候执行。

解决方法:使用匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
func() {
defer func() {
fmt.Println("func: defer runs")
}()
fmt.Println("func: ends")
}()
fmt.Println("main: ends")
}
// output:
func: ends
func: defer runs
main: ends

Deferred method陷阱

不使用指针:

1
2
3
4
5
6
7
8
9
10
11
type Car struct {
model string
}
func (c Car) PrintModel() {
fmt.Println(c.model)
}
func main() {
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel()
c.model = "Chevrolet Impala"
}

输出:

1
DeLorean DMC-12

使用指针:

1
2
3
func (c *Car) PrintModel() {
fmt.Println(c.model)
}

输出:

1
Chevrolet Impala

当使用defer时,传递给函数的参数会被立即保存下来而不用等到函数执行。

当一个方法的接收者是值接收者时,这个接收者会被拷贝(当调用defer函数的时候)所以当修改Car结构的数据被修改后,defer调用的函数并不会知道,因为它使用的是拷贝过来的数据。

如果一个方法的接收者是指针,当调用defer时,虽然也会产生一个新的指针,但这和原因的指针指向的是同一个对象,所以任何的对于结构体Car中的任何改变都能被探测到。

打印Z —> A

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

//output: 3210

执行for循环的时候进行压栈操作,当函数结束的时候弹出然后执行压栈的函数

参数作用域问题,参数被覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type reader struct {}

func (r reader) Close() error {
return errors.New("close Error")
}
func release(r io.Closer) (err error) {
defer func() {
if err := r.Close();err != nil{
...
}
}()
return
}

func main() {
r := reader{}
err := release(r)
fmt.Print(err)
}
//output: nil

也许的你期待的返回是“close Error“,但实际返回的err却是nil。

原因:在if代码块中使用新的err覆盖了name result中的err值,所以release()返回了原来的result-value。
这里还有疑惑,既然被被覆盖了难道不应该返回不为nil的值吗

解决方案:使用=而不:=

1
2
3
4
5
6
7
8
func release(r io.Closer) (err error)  {
defer func() {
if err = r.Close();err != nil{
fmt.Println("err in r.close")
}
}()
return
}

即时计算参数的值

传递给deferred func的参数是在函数注册(调用defer)的时候进行计算的而不是当它执行的时候计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type message struct {
content string
}
func (p *message) set(c string) {
p.content = c
}
func (p *message) print() string {
return p.content
}

func main() {
m := &message{content: "Hello"}
defer fmt.Print(m.print())
m.set("World")
// deferred func runs
}

//output: Hello

为什么输出不是”World”
在调用defer时,fmt.Print是在函数结束之前执行,但是传递给它的参数m.print()会立即执行,所以传递给fmt.Print的参数是”Hello”,而且这个值会被保存直到defer中的函数执行。

for循环中的捕获

在循环中deferred func将会看到最新的值当函数执行的时候,有一种情况除外,就是把值当作参数传递给了deferred func。

1
2
3
4
5
6
7
8
9
10
11
func main() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
//output
3
3
3

Why?
当defer中的函数运行时,deferred func看到的是i的最新值。因为当调用defer进行函数注册时,Go运行时捕获的是变量i的地址。当循环结束后i的值变为3,所以当运行defer中的函数时因为指针指向的是同一个值所以输出都为3。

解决方案1:将值当作参数传递给defer中的函数

1
2
3
4
5
6
7
8
9
10
11
func main() {
for i := 0; i < 3; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
}
//output:
2
1
0

解决方案2:使用一个新的变量i覆盖掉外一层的i

1
2
3
4
5
6
7
8
func main() {
for i := 0; i < 3; i++ {
i := i
defer func() {
fmt.Println(i)
}()
}
}

解决方案3:如果只有一个函数调用

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

defer中函数的返回值

defer函数中的返回值对于调用者是不可见,但你仍可以使用命名返回值的方法改变结果值。

1
2
3
4
5
6
7
8
9
10
11
func release() error {
defer func() error {
return errors.New("error")
}()
return nil
}
func main() {
r := release()
fmt.Println(r)
}
//outpu:<nil>

解决方案:改变命名结果值

1
2
3
4
5
6
func release() (err error) {
defer func() {
err = errors.New("error")
}()
return nil
}

在deferred func之后调用recover函数

1
2
3
4
5
6
func main() {
recover()
panic("error")
}
//output:
panic: error

在defer之外使用recover()不能捕获panic。
解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
func do() {
defer func() {
r := recover()
fmt.Println("recovered:", r)
}()
panic("error")
}

func main() {
do()
}
//output
recovered: error

错误的顺序调用defer func

1
2
3
4
5
6
7
8
9
10
11
12
func do() error {
res, err := http.Get("http://notexists")
defer res.Body.Close()
if err != nil {
return err
}
// ..code...
return nil
}
//output

panic: runtime error: invalid memory address or nil pointer dereference

原因是我们没有检查http请求是否成功,如果失败则会出现如上错误。因为出错的情况下res为nil,当执行res.Body的时候则会引发panic。

解决方案:加个判断

1
2
3
4
5
6
7
8
9
10
11
func do() error {
res, err := http.Get("http://notexists")
if res != nil {
defer res.Body.Close()
}
if err != nil {
return err
}
// ..code...
return nil
}

不检查错误

不要以后把善后的工作委派给了defer就可以安全的释放资源。你有可能会丢失一些有用的报错信息。

不推荐:
f.Close()可能会报错,但是我们意识不到。

1
2
3
4
5
6
7
8
9
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer f.Close()
// ..code...
return nil
}

更好的做法是检查错误并处理错误

1
2
3
4
5
6
7
8
9
10
11
12
13
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
// ..code...
return nil
}

你还可以使用命名结果值返回defer中的错误020年10月19日

1
2
3
4
5
6
7
8
9
10
11
12
13
func do() (err error) {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer func() {
if ferr := f.Close(); ferr != nil {
err = ferr
}
}()
// ..code...
return nil
}

Note:
你可以使用这个包包裹多种错误。这是有必要的,因为f.Close可能会覆盖在其之前的错误。在一个错误中包裹另一个错误并打印到日志中可以更好的排查错误。
你也可以使用这个包来捕获你不想检查的错误

释放相同的资源

你可能会对一个资源进行多次释放操作,这会发生预料之后的问题。

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
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
return nil
}
//output
closing resource #another-book.txt
closing resource #another-book.txt

WHY?
因为当deferred func运行时,看到的是最新的值,所以看到的变量f是最新的那个(another-book.txt)。因为同一资源被释放了两次。

解决方案:

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
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
defer func(f io.Closer) {
if err := f.Close(); err != nil {
// log etc
}
}(f)
return nil
}

//output:
closing resource #another-book.txt
closing resource #book.txt

panic/recover可以获取可返回任何类型

字符串:

1
2
3
4
5
6
7
8
9
10
func errorly() {
defer func() {
fmt.Println(recover())
}()
if badHappened {
panic("error run run")
}
}
//output:
"error run run"

Error:

1
2
3
4
5
6
7
8
9
10
func errorly() {
defer func() {
fmt.Println(recover())
}()
if badHappened {
panic(errors.New("error run run")
}
}
//output:
"error run run"

可接收任何类型的参数
panic不仅可以接收字符串也可以接收error类型。这意味着你可以把任何类型的参数传递给panic然后从recover中获取。

1
2
3
4
5
6
7
8
9
10
11
12
type myerror struct {}
func (myerror) String() string {
return "myerror there!"
}
func errorly() {
defer func() {
fmt.Println(recover())
}()
if badHappened {
panic(myerror{})
}
}

WHY?
在GO中interface{}类型意味着任何类型.
panic和recover的定义

1
2
func panic(v interface{})
func recover() interface{}

它们的工作流程如下
panic(value) —> recover() —> value

recover只是返回传递给panic的值


REF:

Go defer 1
Go defer 2
Go defer 3