使用 defer 时可能遇到的若干陷阱

sunzhaohao · 2018-04-13 21:51:06 · 2364 次点击 · 预计阅读时间 3 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-04-13 21:51:06 的文章,其中的信息可能已经有所发展或是发生改变。

go 的 defer 语句对改善代码可读性起了很大作用。但是,某些情况下 defer 的行为很容易引起混淆,并且难以快速理清。尽管作者已经使用 go 两年多了,依然会被 defer 弄得挠头不已。我的计划是把过去曾困惑过我的一系列行为汇总起来,作为对自己的警示。

defer 的作用域是一个函数,不是一个语句块

一个变量只存在于一个语句块的作用域内。 defer 语句所在的语句块只会在函数返回时执行。我不清楚背后的原理,但是,如果你在一个循环里面先分配资源,再用 defer 回收资源,就有可能带来猝不及防的灾难后果。

func do(files []string) error {
    for _, file := range files {
        f, err := os.Open(file)
        if err != nil {
            return err
        }
        defer f.Close() // 这是错误用法!!
        // 使用 f
    }
}

(译者注:上述代码会造成循环结束后才开始回收资源,而不是执行了一次循环就回收一次资源)

方法链

如果你在一个 defer 语句中链式调用方法,那么除了最后一个函数以外其余函数都会在调用时直接执行。 defer 要求一个函数作为 “参数” 。

type logger struct {}
func (l *logger) Print(s string) {
    fmt.Printf("Log: %v\n", s)
}
type foo struct {
    l *logger
}
func (f *foo) Logger() *logger {
    fmt.Println("Logger()")
    return f.l
}
func do(f *foo) {
    defer f.Logger().Print("done")
    fmt.Println("do")
}

func main() {
    f := &foo{
        l: &logger{},
    }
    do(f)
}

输出结果——

Logger()
do
Log: done

Logger() 函数在 do() 函数之前就已经执行了。

函数参数

嗯,那么如果最后一个函数接收了一个参数,结果又会怎么样呢? ? 按照常理来说 ,如果它是在外围函数返回后才执行,那么对变量的任何修改都会被捕获到,事实真的如我们所料么?

type logger struct {}
func (l *logger) Print(err error) {
    fmt.Printf("Log: %v\n", err)
}
type foo struct {
    l *logger
}
func (f *foo) Logger() *logger {
    fmt.Println("Logger()")
    return f.l
}
func do(f *foo) (err error) {
    defer f.Logger().Print(err)
    fmt.Println("do")
    return fmt.Errorf("ERROR")
}

func main() {
    f := &foo{
        l: &logger{},
    }
    do(f)
}

猜猜输出结果是?

Logger()
do
Log: <nil>

err 的值仍然是调用 defer 时候的值。任何对这个变量的修改都不会被 defer 语句捕获,因为它们并不指向同一个值。

针对非指针类型调用函数

我们已经看到了 defer 语句中链式方法的执行特点。进一步深入下去,如果被调用的方法并不是定义在一个指针类型上,那么将会在 defer 语句中复制出一个新的实例。

type metrics struct {
    success bool
    latency time.Duration
}
func (m metrics) Log() {
    fmt.Printf("Success: %v, Latency: %v\n", m.success, m.latency)
}
func foo() {
    var m metrics
    defer m.Log()
    start := time.Now()
    // Do something
    time.Sleep(2*time.Second)

    m.success = true
    m.latency = time.Now().Sub(start)
}

输出结果

Success: false, Latency: 0s

当 defer 语句执行时 m 被复制。 m.Foo() 实质是 Foo(m) 的简写形式。

结论

如果你使用 go 的时间足够久,那么这些可能都算不上 “陷阱” 。但对新手来说, defer 语句的很多地方都不符合 最少吃惊原则 。还有 深入研究了使用 go 时可能遇到的常见失误。欢迎阅读。


via: https://medium.com/@i0exception/some-common-traps-while-using-defer-205ebbdc0a3b

作者:Aniruddha  译者:sunzhaohao  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2364 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传