多个defer语句 vs 延迟的匿名函数

45

在顺序相依的情况下,是否更安全或更符合惯用法发出多个 defer 语句,还是将处理逻辑封装在匿名函数中并推迟执行?

示例:

defer os.Remove(tempFile.Name())
defer tempFile.Close()

在上面的情况中,语法很简单,但是延迟执行的顺序与实际逻辑相反。

在下面的情况中,有更多的行数、更多的“语法”,但逻辑顺序更加自然:

defer func() {
    tempFile.Close()
    os.Remove(tempFile.Name())
}()

使用哪一个?


5
仅为一小笔注释(不值得完整回答):defer 会带来性能损耗。它非常微小(根据我在 go1.5.1 linux/amd64 上的测试大约为 75 纳秒),可能不值得考虑,但仍应该指出。 - HectorJ
2个回答

43
在这个例子中,匿名函数更容易阅读,特别是一旦你加入错误处理。
f, err := ioutil.TempFile("", "prefix")
if err != nil {
  log.Println("creating temp file:", err)
  return
}
defer func() {
  err := f.Close()
  if err != nil {
    log.Println("close:", err)
  }
  err = os.Remove(f.Name())
  if err != nil {
    log.Println("remove:", err)
  }
}()

如果您有多个资源,则通常适用多个 defer


17
根据Ross Light的回答所述

如果您有多个资源,那么多个延迟通常是合适的。

2019年4月:但在这种情况下,请考虑Go 1.13(2019年第四季度),因为它集成了对go问题14939:“运行时:defer很慢”go问题6980:“cmd/compile:在堆栈帧中分配一些延迟”的修复。
请参阅Go CL 171758:“cmd/compile,runtime:在堆栈上分配延迟记录”

When a defer is executed at most once in a function body, we can allocate the defer record for it on the stack instead of on the heap.

This should make defers like this (which are very common) faster.

This optimization applies to 363 out of the 370 static defer sites in the cmd/go binary.

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

2019年10月(Go 1.13几周前发布)
这个已经得到确认(Brad Fitzpatrick),与CL 190098相符:

Cost of defer statement [ go test -run NONE -bench BenchmarkDefer$ runtime ]

With normal (stack-allocated) defers only:         35.4  ns/op
With open-coded defers:                             5.6  ns/op
Cost of function call alone (remove defer keyword): 4.4  ns/op
但是达米安·格里斯基补充道

Defer gets cheaper, but panic/recover is more expensive.

Cost of defer: 34ns -> 6ns.
Cost of panic/recover: 62ns -> 255ns
这不是一个坏的交换。
换句话说,虽然使用多个defer可以成为惯用法,但这种做法由于性能成本而受到限制。然而,在Go 1.13+中,性能成本已不再是问题。(正如Paschalis的博文"defer是什么?你可以同时运行多少个?"所示)
这使得在代码流无关的位置执行函数调用成为defer实际应用的可能。
然而,John Refior指出defer是同步的
实际上,defer会在函数退出之前立即执行。
它是同步的,所以调用者会等待defer完成。
因此,即使现在可以使用多个defer,请确保它们执行速度快,或者正如John所指出的那样:

Fortunately it’s easy to wrap a goroutine in a defer, giving us the flow control and timing we want, without delaying the caller:

func Handler(w http.ResponseWriter, r *http.Request) {
    log.Println("Entered Handler")
    defer func() {
        go func() {
            time.Sleep(5 * time.Second)
            log.Println("Exiting goroutine")
        }()
        log.Println("Exiting defer")
    }()
}

Often defers are used for locking a mutex, or closing a connection or file descriptor, and the work they do is fast, or we want it to complete before the caller moves on.

But when you’re doing slow work that the client shouldn’t need to wait for at the end of an HTTP handler, making the call asynchronous can substantially improve user experience.


1
有人能把这个转化成一个真正回答问题的答案,并且给出底线结论吗?“2019年4月:但在这种情况下,请考虑Go 1.13…”似乎并没有真正反驳“如果您有多个资源,则通常适用多个defer”的说法。它只是提供了一些关于开销的背景信息,对吧? - Benjamin R
2
非常感谢您的澄清,现在这是一个完整而出色的答案。 - Benjamin R

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