前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang defer关键字的使用

golang defer关键字的使用

作者头像
山行AI
发布2019-11-21 11:21:42
5950
发布2019-11-21 11:21:42
举报
文章被收录于专栏:山行AI山行AI

defer的触发时机

代码语言:javascript
复制
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触发的时机:

  1. 所在函数return时;
  2. 所在函数执行结束时;
  3. 所在goroutine发生恐慌(panic)时

goland defer的使用方式

在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

为了更好的学习defer的行为,我们首先来看下面一段代码:

代码语言:javascript
复制
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
}

这段代码可以运行,但存在'安全隐患'。如果调用dst, err := os.Create(dstName)失败,则函数会执行return退出运行。但之前创建的src(文件句柄)没有被释放。上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。而使用defer则可以避免这种情况的发生,下面是使用defer的代码:

代码语言:javascript
复制
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作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。

规则一 当defer被声明时,其参数就会被实时解析

我们通过以下代码来解释这条规则:

代码语言:javascript
复制
func a() {
i := 0
defer fmt.Println(i)
i++
return
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

读者自行编译看一下,结果输出的是0. why?

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。换言之,上面的代码等同于下面的代码:

代码语言:javascript
复制
func a() {
i := 0
defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
i++
return
}

为了更为明确的说明这个问题,我们继续定义一个defer:

代码语言:javascript
复制
func a() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0
i++
defer fmt.Println(i) //输出1,因为i此时就是1
return
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出1,在输出0呢?看下面的规则二。

规则二 defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。不要为什么,golang就是这么定义的。我们用下面的代码加深记忆和理解:

代码语言:javascript
复制
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}

在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。安装先进后出的原则,我们可以看到依次输出了3210.

规则三 defer可以读取有名返回值

先看下面的代码:

代码语言:javascript
复制
func c() (i int) {
defer func() { i++ }()
return 1
}

输出结果是2. 在开头的时候,我们说过defer是在return调用之后才执行的。这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢....)。

当执行return 1 之后,i的值就是1. 此时此刻,defer代码块开始执行,对i进行自增操作。因此输出2.

掌握了defer以上三条使用规则,那么当我们遇到defer代码块时,就可以明确得知defer的预期结果。

常见的问题

问题一 匿名返回值和命名返回值的不同

代码语言:javascript
复制
func main()  {
    //lifoDefers()

    m1 := returnValues()
    fmt.Println(m1)

    m2 := namedReturnValues()
    fmt.Println(m2)
}

func returnValues() int {
    var result int
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

func namedReturnValues() (result int) {
    defer func() {
        result++
        fmt.Println("defer")
    }()
    return result
}

输出0 1,原因是return 和 defer不是同时执行,而是 在return更新完返回值之后再去执行defer,可以用这个特性来观察返回值。在go语言圣经5.8章 198页可以查到,它会先更新返回值,再执行defer函数,因为返回值匿名,所以更新的是result变量,更新后的result没有赋值给返回值,所以结果为0。而命名返回值的函数,一直操作的是返回值,在defer中也是操作的defer,所以最后在defer执行完毕返回的就是最新的返回值。

问题二 在for循环中使用defer可能导致的性能问题

代码语言:javascript
复制
func deferInLoops() {
    for i := 0; i < 100; i++ {
        f, _ := os.Open("/etc/hosts")
        defer f.Close()
    }
}

defer在紧邻创建资源的语句后生命力,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。

问题三 判断执行没有err之后,再defer释放资源

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确写法如下。

代码语言:javascript
复制
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
    return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

问题四 调用os.Exit时defer不会被执行

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

代码语言:javascript
复制
func deferExit() {
    defer func() {
        fmt.Println("defer")
    }()
    os.Exit(0)
}

上面的defer并不会输出。

整理自

  • https://studygolang.com/articles/10167
  • https://www.jianshu.com/p/79c029c0bd58

有发现更多的defer使用上的问题与案例会继续更新

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • defer的触发时机
  • goland defer的使用方式
    • 规则一 当defer被声明时,其参数就会被实时解析
      • 规则二 defer执行顺序为先进后出
        • 规则三 defer可以读取有名返回值
        • 常见的问题
          • 问题一 匿名返回值和命名返回值的不同
            • 问题二 在for循环中使用defer可能导致的性能问题
              • 问题三 判断执行没有err之后,再defer释放资源
                • 问题四 调用os.Exit时defer不会被执行
                • 整理自
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档