您的位置 首页 golang

「GCTT 出品」图解 Go 中的延迟调用 defer

什么是 defer ?

通过使用 defer 修饰一个函数,使其在外部函数 “返回后” 才被执行,即便外部的函数返回的是 panic 异常,这类函数被称作 延迟调用函数。

打印: “first” 然后 “later”


  • Go 语言并不需要析构函数因为其本身并没有自带构造函数,这是一个很好的取舍。

  • defer 语句与 finally 类似,但两者的不同是 finally 的作用域在其 异常块 中,而 defer 的作用域则限于包围它的那个函数。


更多关于 defer: 如果你好奇 defer 的内部机制是如何工作的,请查看 我的评论 。虽然它在 官方文档 中总是被描述为 “在外部函数返回后执行”,但其中还有些不为人知的细节未被解释清楚。


defer 的常见用途

释放已取得的资源

使用 defer 的延迟调用函数经常被用于在函数体内释放已获取的资源。

这个延迟函数关闭了已经打开的 文件句柄 ,不论 NewFromFile 函数是否返回了错误。

从 panic 中恢复

如果 defer 和被触发的 panic 位于同一个 goroutine 中, defer 能够使程序从 panic 中恢复,避免整个程序终止。

其中的 recover() 函数能够返回 panic() 函数的参数,这就使得我们能自行处理 panic ,同时你也可以向 panic 中传入错误或其他类型的值来判断引发 panic 的究竟是哪一个值。 更多详情

延迟 闭包

一个使用了 defer 的延迟调用函数可以是任意类型的函数,因此,当 defer 作用于一个 匿名函数 的时候,这个函数就能够获取到外围函数体中的变量的最新状态。

值得注意的是,下面的一个例子展示了延迟调用的匿名函数可以获取到 局部变量 num 的最终状态。

理解 defer 函数是如何对待它的上下文的

参数即时求值

Go 的运行时会在延迟调用函数声明时保存任何传递到延迟调用函数中的参数,而不是当它被运行的时候。


示例

在下面的例子中,我们定义了一个延迟闭包,其使用了上面的同名变量 n 并试图将 i 变量再次传入延迟函数来增加变量 n 的值。

func count(i int) (n int) {
 defer func() {
 n = n + i
 }(i)

 i = i * 2
 n = i return} 

我们运行这个函数看看

count(10)// 输出:30 

发生了什么?

分析可视化数字(在左边): 1, 2, 3 .

译者注:可以发现传入的延迟函数的 i 变量在 count() 返回之前就已经被运行时记录了其拷贝值(也就是 10 ),即便在 count() 返回后闭包内使用的 i 变量依然是之前的拷贝值。因此,上图中第 3 步 i 应该是 10,而不是 20,应该是作者笔误。


上述的例子表明,通过指定返回值变量名, defer 还能够帮助我们在函数返回之前改变返回值的结果。

延迟调用多个函数

如果有多个延迟函数,它们会被存储在一个 栈 中,因此,最后被 defer 修饰的函数会在函数体返回之后先执行。

注意:同时使用多个 defer 表达式可能会降低代码的可读性

如下图所示:

输出结果如下

first
last 

理解多个 defer 是如何工作的

观察它是如何工作的

延迟调用对象的方法

你也可以使用 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” 指针的地址相同。因此,任何修改都会完美地作用在同一个对象中。


via:

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

文章来源:智云一二三科技

文章标题:「GCTT 出品」图解 Go 中的延迟调用 defer

文章地址:https://www.zhihuclub.com/100048.shtml

关于作者: 智云科技

热门文章

网站地图