Please enable Javascript to view the contents

Go-Defer说明

 ·  ☕ 3 分钟

重要

defer语句是Go中一个非常有用的特性,可以将一个方法延迟到包裹defer的方法返回时执行,在实际应用中,defer可以充当其他语言中try…catch…的角色,
也可以用来处理关闭文件句柄等收尾操作。

1. defer使用

1.1 defer触发的时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

官方文档指出,执行defer的时机为:

  • 包裹defer的函数返回时
  • 包裹defer的函数执行到末尾时
  • 所在的goroutine发生panic

1.2 defer执行顺序

当一个方法中有多个defer时, defer会将要延迟执行的方法压栈,当defer被触发时,将所有压栈的方法出栈并执行。
所以defer的执行顺序是LIFO的。
例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func stackingDefers() {
    defer func() {
        fmt.Println("1")
    }()
    defer func() {
        fmt.Println("2")
    }()
    defer func() {
        fmt.Println("3")
    }()
}

结果为:

1
2
3
3
2
1

2. 坑

2.1 返回值是否匿名表现不同

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 匿名返回值 (int),输出 0
func returnValues() int {
    var result int
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

// 命名返回值(result int), 输出 1
// 因为return与defer是同时触发。
func namedReturnValues() (result int) {
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

匿名返回执行顺序为:

  1. 将result赋值给返回值(相当于, returnValue=result=0);

  2. 检查到defer,执行defer的语句,变量result值+1(result=1);

  3. 返回函数返回值,returnValue(returnValue=0)。

命名返回值执行顺序为:

  1. 返回值被声明为result
  2. 检查到defer,执行defer的语句,变量result值+1(result=1);
  3. 返回函数返回值,result(result=1)。

注意与python的浅拷贝区分开

2.2 for循环中使用可能导致的性能问题

比如:

1
2
3
4
5
6
func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

此处defer f.Close()在创建文件对象后声明,看起来没有什么问题。但是和直接调用f.Close()相比,defer的执行存在着额外的开销。

defer会存在两方面的资源开销:

  • defer对其后需要的参数进行内存拷贝;
  • 还需要对defer结构进行压栈出栈操作。

可以将f.Close()语句前的defer去掉,直接调用f.close()来减少大量defer导致的额外资源消耗。

2.3 必须判断执行成功,再defer释放资源

如果获取资源的操作返回err参数:

  • 不使用defer, 可以选择忽略返回的err参数;

  • 使用defer进行延迟释放, 在使用defer之前先判断是否存在err。

资源没有获取成功,对资源执行释放操作会导致释放方法执行报错。必须判断是否获取成功,再释放资源。

正确写法:

1
2
3
4
5
6
7
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

2.4 调用os.Exit时defer不会被执行

当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer不会执行。

下面例子中defer不会输出:

1
2
3
4
5
6
func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

Reference

Go语言中defer的一些坑

分享

Hex
作者
Hex
CloudNative Developer

目录