Go 中 defer 的 5 个坑 - 第一部分

yujiahaol68 · 2017-12-26 15:11:12 · 18317 次点击 · 预计阅读时间 5 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2017-12-26 15:11:12 的文章,其中的信息可能已经有所发展或是发生改变。

通过本节的学习以避免掉入基础的 defer 陷阱中

本文只适合想要进阶学习 Golang 的新手阅读,大牛请绕道。

#1 -- defer nil 函数

如果一个延迟函数被赋值为 nil , 运行时的 panic 异常会发生在外围函数执行结束后而不是 defer 的函数被调用的时候。

例子

func() {
  var run func() = nil
  defer run()

  fmt.Println("runs")
}

输出结果

runs

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

发生了什么?

名为 func 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在外围函数运行完成后它才会被调用。

上面只是一个简单的案例,但同样的案例也可能发生在真实世界中,所以如果你遇上的话,可以想想是不是掉进了这个坑里。

#2 -- 在循环中使用 defer

切忌在循环中使用 defer,除非你清楚自己在做什么,因为它们的执行结果常常会出人意料。

但是,在某些情况下,在循环中使用 defer 会相当方便,例如将函数中的递归转交给 defer,但这显然已经不是本文应该讲解的内容。

在上面的例子中,defer row.Close() 在循环中的延迟函数会在函数结束过后运行,而不是每次 for 循环结束之后。这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。

解决方案 #1:

不使用 defer ,直接在末尾调用。

解决方案 #2:

将任务转交给另一个函数然后在里面使用 defer,在下面这种情况下,延迟函数会在每次匿名函数执行结束后执行。

进行基准测试

查看代码

#3 -- 延迟调用含有闭包的函数

有时出于某种缘由,你想要让那些闭包延迟执行。例如,连接数据库,然后在查询语句执行过后中断与数据库的连接。

例子

type database struct{}

func (db *database) connect() (disconnect func()) {
  fmt.Println("connect")

  return func() {
    fmt.Println("disconnect")
  }
}

运行一下

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

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

输出结果

query db...
connect

竟然出问题了?

最终 disconnect 并没有输出,最后只有 connect ,这是一个 bug,最终的情况是 connect() 执行结束后,其执行域得以被保存起来,但内部的闭包并不会被执行。

解决方案

func() {
  db := &database{}
  close := db.connect()
  defer close()

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

稍作修改后, db.connect() 返回了一个函数,然后我们再对这个函数使用 defer 就能够在 func() 执行结束后断开与数据库的连接。

输出结果

connect
query db...
disconnect

糟糕的处理方式:

即便这种处理方式很糟,但我还是想告诉你如何不用变量来解决这个问题,因此,我希望你能以此来了解 defer 亦或是 go 语言的运行机制。

func() {
  db := &database{}
  defer db.connect()()

  ..
}

这段代码从技术层面上说与上面的解决方案没有本质区别。其中,第一个圆括号是连接数据库(在 defer db.connect() 中立即执行的部分),然后第二个圆括号是为了在 func() 结束时延迟执行断开连接的函数(也就是返回的闭包)。

归因于 db.connect() 创建了一个闭包类型的值,然后再使用 defer 声明闭包函数, db.connect() 的值需要被实现计算出来以便让 defer 知道需要延迟哪个函数,这与 defer 不直接相关但也可能帮助你解决一些问题。

#4 -- 在执行块中使用 defer

你可能想要在执行块执行结束后执行在块内延迟调用的函数,但事实并非如此,它们只会在块所属的函数执行结束后才被执行,这种情况适用于所有的代码块除了上文的函数块例如,for,switch 等。

因为:延迟是相对于一个函数而非一个代码块

例子

func main() {
  {
    defer func() {
      fmt.Println("block: defer runs")
    }()

    fmt.Println("block: ends")
  }

  fmt.Println("main: ends")
}

输出结果

block: ends
main: ends
block: defer runs

上例的延迟函数只会在函数执行结束后运行,而不是紧接着它所在的块(花括号内包含 defer 调用的区域)后执行,就像代码中的演示的那样,你可以使用花括号创造单独的执行块。

另一个解决方案

如果你希望在另一个块中使用 defer ,可以使用匿名函数(正如在第二个坑中我们采用的解决方案)。

func main() {
  func() {
    defer func() {
      fmt.Println("func: defer runs")
    }()

    fmt.Println("func: ends")
  }()

  fmt.Println("main: ends")
}

#5 -- 延迟方法的坑

同样,你也可以使用 defer 来延迟 方法 调用,但也可能出一些岔子。

没有使用指针作为接收者

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"
}

输出结果

DeLorean DMC-12

使用指针对象作为接收者

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

输出结果

Chevrolet Impala

为什么会这样?

我们需要记住的是,当外围函数还没有返回的时候,Go 的运行时就会立刻将传递给延迟函数的参数保存起来。

因此,当一个以值作为接收者的方法被 defer 修饰时,接收者会在声明时被拷贝(在这个例子中那就是 Car 对象),此时任何对拷贝的修改都将不可见(例中的 Car.model ),因为,接收者也同时是输入的参数,当使用 defer 修饰时会立刻得出参数的值(也就是 "DeLorean DMC-12" )。

在另一种情况下,当被延迟调用时,接收者为指针对象,此时虽然会产生新的指针变量,但其指向的地址依然与上例中的 "c" 指针的地址相同。因此,任何修改都会完美地作用在同一个对象中。

以上就是本文的全部内容,我会在后续的文章中补充更多类似的坑 -- 已经有至少 15 个易犯的 defer 错误榜上有名,如果你有任何想法,欢迎在下面留言。


via: https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01

作者:Inanc Gumus  译者:yujiahaol68  校对:rxcai

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


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

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

18317 次点击  ∙  1 赞  
加入收藏 微博
被以下专栏收入,发现更多相似内容
9 回复  |  直到 2019-06-08 14:02:11
1034992601
1034992601 · #1 · 7年之前
func main() {

 //1
  func() {
    defer func() {
      fmt.Println("func: defer runs")
    }()

    fmt.Println("func: ends")
  }()

  fmt.Println("main: ends")
}

按你这样写的话也有问题,一般会在//1处加上业务代码,如果出错了下面是运行不到的,如果将defer的func放前面, 业务代码还没有 运行又作了回收工作

我感觉defer用久了,感觉越来越不敢乱用了

javasgl
javasgl · #2 · 7年之前
10349926011034992601 #1 回复

```go func main() { //1 func() { defer func() { fmt.Println("func: defer runs") }() fmt.Println("func: ends") }() fmt.Println("main: ends") } ``` 按你这样写的话也有问题,一般会在//1处加上业务代码,如果出错了下面是运行不到的,如果将defer的func放前面, 业务代码还没有 运行又作了回收工作 我感觉defer用久了,感觉越来越不敢乱用了

func main() {

 //1业务代码出错了
 defer func() {
    defer func() {
      fmt.Println("func: defer runs")
    }()

    fmt.Println("func: ends")
  }()

  fmt.Println("main: ends")
}

如果这func 前面再加一个 defer, 当 //1的业务代码出错了. func中的代码还是能执行的吧

1034992601
1034992601 · #3 · 7年之前
javasgljavasgl #2 回复

#1楼 @1034992601 ``` func main() { //1业务代码出错了 defer func() { defer func() { fmt.Println("func: defer runs") }() fmt.Println("func: ends") }() fmt.Println("main: ends") } ``` 如果这func 前面再加一个 defer, 当 //1的业务代码出错了. func中的代码还是能执行的吧

你说的在func前面加上一个defer, 那样的话func里面的内容其实是在main执行结束前才会运行, 没有达到想要的效果,

{
  a()
  defer内容
  b()
  c()
}

想要的是a()运行之后就要运行defer, 而不是运行b(),c()之后才去运行defer, 说白了就是快速的释放一些东西

1034992601
1034992601 · #4 · 7年之前
javasgljavasgl #2 回复

#1楼 @1034992601 ``` func main() { //1业务代码出错了 defer func() { defer func() { fmt.Println("func: defer runs") }() fmt.Println("func: ends") }() fmt.Println("main: ends") } ``` 如果这func 前面再加一个 defer, 当 //1的业务代码出错了. func中的代码还是能执行的吧

你说的在func前面加上一个defer, 那样的话func里面的内容其实是在main执行结束前才会运行, 没有达到想要的效果,

 { a() defer内容 b() c() }

想要的是a()运行之后就要运行defer, 而不是运行b(),c()之后才去运行defer, 说白了就是快速的释放一些东西

javasgl
javasgl · #5 · 7年之前
10349926011034992601 #4 回复

#2楼 @javasgl 你说的在func前面加上一个defer, 那样的话func里面的内容其实是在main执行结束前才会运行, 没有达到想要的效果, ``` go { a() defer内容 b() c() } ``` 想要的是a()运行之后就要运行defer, 而不是运行b(),c()之后才去运行defer, 说白了就是快速的释放一些东西

a之后就运行 defer 就没必要用 defer了, 直接写要运行的代码就行了。

{
   a()
   //relase resouce
   b()
   c()
}

这种场景下没有上使用 defer 的必要了

javasgl
javasgl · #6 · 7年之前

要想 a() 出错了, release resource 这步还能执行的话,那就只能把 release resource 放到 defer 中去了,如果放到 defer 中, 它就只能在 main 结束的时候执行了,又达不到你要求的立即释放的要求 如果要求出错后还能立即执行,这有点矛盾,毕竟 没有 try....catch 这种语法

xinhaota
xinhaota · #7 · 7年之前

第一个例子后面少了一对 ()吧? func() { var run func() = nil defer run()

fmt.Println("runs") }()

polaris
polaris · #8 · 7年之前
xinhaotaxinhaota #7 回复

第一个例子后面少了一对 ()吧? func() { var run func() = nil defer run() fmt.Println("runs") }()

这个理解为部分代码,实际代码中确实需要加上 ()

yanjinbin-del
yanjinbin-del · #9 · 6年之前

说真的 这篇文章 很水
理解defer 你看 官网blog spec 以及GCTT处的Go tutorial 对于defer 闭包变量的传递 可以清楚的理解defer 的用法 , 坑不坑都看你自己的理解
作者说的这些东西 都是经验之谈 并没有 insight go 的任何一点原理or 特性

评价: 1颗星

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传