go defer 的使用和陷阱

目录

前言

初学 go 的同学都应该了解 defer, defer 让人又爱又恨;当 defer 和 闭包结合在一起的时候更是一大杀器, 会用的人是伤敌一万,而不会用的人是自损八千

本文希望从一个个问题来带大家重新认识 defer。

先抛出一个简单的问题来引入全文,下面程序正确的输出内容是什么?

package main

import "fmt"

func main() {
    fmt.Println(f1())
    fmt.Println(f2())
}

func f1() (n int) {
    n = 1
    defer func() {
        n++
    }()
    return n
}

func f2() int {
    n := 1
    defer func() {
        n++
    }()
    return n
}

输出:

2
1

如果对上面内容有疑惑,没关系我们开始进入正文

正文

什么是 defer

defer 是Go语言提供的一种用于注册延迟调用的机制,以用来保证一些资源被回收和释放

defer 注册的延迟调用可以在当前函数执行完毕后执行(包括通过return正常结束或者panic导致的异常结束)

当defe注册了的函数或表达式逆序执行,先注册的后执行,类似于 ”先进后出“

下面看一个例子:

package main

import "fmt"

func main() {
   f()
}

func f() {
   defer func() {
      fmt.Println(1)
   }()
   defer func() {
      fmt.Println(2)
   }()
   defer func() {
      fmt.Println(3)
   }()
}

输出:

3
2
1

如何使用defer

释放资源

使用 defer 可以在一定程度上避免资源泄漏,尤其是有很多 return 语句的场景,很容易忘记或者由于逻辑上的错误导致资源没有关闭。

下面的程序便是因为使用 return 后,关闭资源的语句没有执行,导致资源泄漏:

f, err := os.Open("test.txt")
if err != nil {
   return
}
f.process()
f.Close()

此处更好的做法如下:

f, err := os.Open("test.txt")
if err != nil {
   return
}
defer f.Close()

// 对文件进行操作
f,process()

此处当程序顺利执行后,defer 会释放资源;defer 需要先注册后使用,比如此处,打开文件异常时,程序执行到 return 语句时便会退出当前函数,没有经过 defer,所以此处defer 不会执行

defer 捕获异常

在 go 中没有 trycatch , 当程序出现异常是,我们需要从异常中恢复。我们这时可以利用 defer + recover 进行异常捕获

func f() {
    defer func() {
       if err := recover(); err != nil {
          fmt.Println(err)
       }
    }()
    // do something
    panic("panic")
}

注意,recover() 函数在在defer中用匿名函数调用才有效,以下程序不能进行异常捕获:

func f() {
    if err := recover(); err != nil {
        fmt.Println(err)
    }
    //  do something
    panic("panic")
}

实现代码追踪

下面提供一个方法能追踪到程序时进入或离开某个函数的信息,此处可以用来测试特定函数有没有被执行

func trace(msg string) { fmt.Println("entering:", msg) }
func untrace(msg string) { fmt.Println("leaving:", msg) }

下面将演示如何使用这两个函数:

func a() {
    defer un(trace("func a()"))
    fmt.Println("in func a()")
}

func trace(msg string) string {
    fmt.Println("entering:", msg)
    return msg
}
func un(msg string) { fmt.Println("leaving:", msg) }

记录函数的参数与返回值

有时候程序返回结果不符合预期是, 大家可能手动打印 log 调试,此时使用 defer 记录函数的参数和返回值,避免手动多处打印调试语句

func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, nil
}

实现代码追踪记录函数的参数与返回值 这两个技巧来自无闻翻译的 《the way to go》,本文稍有改动,详情可以参阅 defer 和追踪

defer 的陷阱

defer 和 函数返回值

情况1:

这里列出开头的例子:

func f1() (n int) {
    n = 1
    defer func() {
        n++
    }()
    return n
}

func f2() int {
    n := 1
    defer func() {
        n++
    }()
    return n
}

这里 f1() 的返回结果是 2, 而 f2() 的返回结果是 1, 为什么会有这样的变化呢?

return xxx 这一条语句并不是一条原子指令, 它编译后的整体的流程是:

  1. 返回值 = xxx
  2. 调用defer函数
  3. 空的 return

记住上面几点可以解决绝大多数使用 defer 后的返回值问题

我们再来看上面 2 个例子:

func f1() (n int) {
    n = 1
    defer func() {
        n++
    }()
    return n
}

分解 f1()

func f1() (n int) {
    // 1. 返回值 = xxx
    n = 1
    
    // 2. 调用defer函数
    func() {
        n++
    }()
    
    // 3. 空的 return
    return
}

所以返回的 n 是被修改过的

继续看 f2():

func f2() int {
    n := 1
    defer func() {
        n++
    }()
    return n
}

分解 f2(),此处惊现中文编程:

func f2() int {
    n := 1
    // 1. 返回值 = xxx
    匿名返回值 = n

    // 2. 调用defer函数
    func() {
        n++
    }()

    // 3. 空的 return
    return
}

此处 defer 注册的匿名函数只能对 n 进行操作,不能影响到所谓的 匿名返回值,所以不会对返回值造成影响。

那么问题来了,如果如果 defer 注册的匿名函数传入 n 的指针会对返回值产生影响吗?

func f2() int {
   n := 1
   defer func(n *int) {
      *n++
   }(&n)
   return n
}

答案是 不会产生影响的,因为在1. 返回值 = xxx 的过程是值赋值,返回值并不会因为 n 的改变而改变

但是如果f2() 中的 n 不是 int 类型而是 slice 的话,结果就会有所不同,defer 注册的匿名函数可以改变底层数组

func f2() []int {
    n := []int{1}
    defer func() {
        n[0] = 2
    }()
    return n
}

此处要留意 值类型和引用类型

希望读者结合上述知识和闭包相关知识,尝试思考下面给出几个例子:

func f3() (n int) {
   n = 1
   defer func(n int) {
      n++
   }(n)
   return n          
}

func f4() (n int) {
   n = 1
   defer func(n *int) {
      *n++
   }(&n)
   return n           
}

type N struct {
    x int
}
func f5() *N {
    n := &N{1}
    defer func() {
        n.x++
    }()
    return n 
}

答案:

f3: n = 1
f4: n = 2
f5: n.x = 2

defer 和 for

在 for 中使用 defer 产生的问题一般比较隐晦,在特定场景下就很致命,先看下面这个例子:

for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 这是错误的方式,当循环结束时文件没有关闭
    defer f.Close()
    // 对文件进行操作
    f.Process(data)
}

循环结尾处的 defer 没有执行,所以文件一直没有关闭

此处敲黑板!defer仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行

更好的做法是:

for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 对文件进行操作
    f.Process(data)
    // 关闭文件
    f.Close()
 }

上述问题自无闻翻译的 《the way to go》,详情可以参阅 发生错误时使用defer关闭一个文件

再看另外一个例子:

func main() {
   for _, file := range files {
      write(file)
   }
}

func write(file string) {
   ...
   if f, err = os.Open(file); err != nil {
      return
   }
   defer f.Close()
   // 对文件进行操作
   f.Process(data)
}

此处关于 defer 用法没有明显问题,但是需要注意的是:defer 会推迟资源的释放,所以尽量不要在 for 中使用;defer 相对于普通函数来说有一定的性能损耗

defer 关闭文件

针对于 defer 关闭文件,这里存在一个问题,举个例子:

f, err := os.Open(file)
if err != nil {
    panic(err)
}
defer f.Close()
// 对文件进行操作
f.process()

在这个例子中貌似没有什么问题,正常情况下,对文件操作之后使用 defer 释放资源,当打开文件失败是引发 panic。但是此处当 打开文件失败时, f 为 nil, 此时使用 defer 释放资源会引发新的 panic

此处更好的做法是:

f, err := os.Open(file)
if err != nil {
    panic(err)
}
if f != nil {
    defer f.Close()
}

defer 和 panic

老规矩,先举例子,判断此处的返回值:

func f() {
    defer func() {
        fmt.Println(1)
    }()
    defer func() {
        fmt.Println(2)
    }()
    panic("panic")
}

输出:

2
1
panic: panic
...

此处我本以为 panic 会先打印出来,然而 panic 是最后打印出来的。此处注意panic会停掉当前正的程序,在这之前,它会有序地执行完当前协程defer列表里的语句

总结

  1. defer 是一种用于注册延迟调用的机制,以用来保证一些资源被回收和释放
  2. defer 注册的函数逆序执行先注册后执行
  3. return xxx 语句不是一条原子指令,使用 defer 时可能会改变返回值
  4. defer 会推迟资源的释放,所以尽量不要在 for 中使用
  5. defer 相对于普通函数来说有一定的性能损耗
  6. defer 注册之后才会执行(放在 return 之后的 defer 不会执行)

最后

以上为编者学习 defer 时,参考多篇文章后的总结。由于能力有限,疏忽和不足之处难以避免,欢迎读者指正,以便及时修改。

若本文对你有帮助的话,请点点赞和转发,感谢你的支持!

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342