Golang的defer行为

60

Effective Go中关于defer的说明如下:

延迟函数的参数(如果函数是方法,则包括接收器)在defer执行时被计算,而不是在调用执行时计算。除了避免担心变量在函数执行时改变值之外,这意味着单个延迟调用站点可以延迟多个函数执行。以下是一个愚蠢的例子。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

延迟函数按照后进先出的顺序执行,因此当函数返回时,该代码将导致 4 3 2 1 0 被打印出来。

4个回答

61

看起来很连贯(另请参见“Defer,Panic和Recover”)

延迟调用按照后进先出的顺序执行,在包围函数返回之后执行。

此函数将打印“3210”:

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

defer被评估的最后一次意味着i=3,倒数第二次意味着i=2以此类推。

Golang规范

  

每次"defer"语句执行时,函数值和调用的参数会像往常一样被评估并保存,但实际的函数体不会被执行。


  

defers将在函数结束时被调用

是的,但它们的参数在循环运行时就被评估了。

在 "How golang's “defer” capture closure's parameter?" 中,您遇到了一个更棘手的 defer 情况,当与闭包 (function literal)一起使用时,详见"Why add “()” after closure body in Golang?"。


那么,这是否意味着在延迟执行中的所有参数都将在代码控制流期间进行评估并推送到堆栈上,只有执行部分会被推迟到函数结束时执行? - remudada
2
是的,根据规范:“每次执行“defer”语句时,函数值和调用参数会像往常一样被评估并保存,但实际的函数体不会被执行。”而且你并不是唯一一个被困惑/惊讶的人。 :) - twotwotwo
1
似乎 defers 会在函数结束时调用(而不是在 for 循环结束时)。 - jtuki

14

在更深入的阅读中,规范还明确指出,参数是在 defer 语句运行时评估的,而不是在实际调用延迟函数时进行返回/恐慌处理时评估:

每次执行 "defer" 语句时,函数值和调用的参数都像通常一样被评估并重新保存,但实际的函数体不会被执行。

是的,这可能确实令人困惑,因为参数在一个时间被评估,而函数体在另一个时间运行。我也曾被它所困扰。


5
我认为你的困惑在于"the defer executes"和"the call executes"这两个短语的含义。我相信"the defer executes"是指控制流到达以defer开头的行时执行的情况,也就是在循环内部发生了五次。相反,"the call executes"是指当fmt.Printf("%d ", i)被执行时的情况,即外围函数返回时。
如果我的解释是正确的,那么你的陈述"since the defers will be called when the for loop ends"是错误的(printf将在循环后调用,但defer在内部被调用),一切都符合其他答案中所解释的行为。

1
我同意。我认为“is executed”,“is called”,“is run”这些术语是含糊不清的。像你解释的那样更容易理解:“当控制流到达以defer开头的行时,延迟执行的函数参数会被计算”。 - Karl Pokus

0
Defer提供了在函数执行后执行代码的机会。例如,在函数终止后关闭资源。为了实现这一目标,defer函数对函数执行进行捕获(捕获函数的变量和状态),然后将defer插入GO队列中。当函数终止时,GO将确定是否有任何队列不为空,并执行defer函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接