英文:
Multiple defers vs deferred anonymous function
问题
在这种情况下,更安全和更符合惯用法的做法是使用依赖于顺序的多个defer
语句,还是使用封装逻辑的匿名函数进行延迟处理?
示例:
defer os.Remove(tempFile.Name())
defer tempFile.Close()
在上述情况下,语法最简洁,但是defer
语句的顺序与要执行的逻辑相反。
在下面的情况下,代码行更多,语法更复杂,但是逻辑顺序更自然:
defer func() {
tempFile.Close()
os.Remove(tempFile.Name())
}()
应该使用哪种方式?
英文:
Is it safer or more idiomatic to issue multiple defer
statements which are dependent on the order, or to defer an anonymous function which packages the logic?
Examples:
<!-- language: lang-go -->
defer os.Remove(tempFile.Name())
defer tempFile.Close()
In the case above the syntax is minimal, yet the order of defers is reverse to the logic to be executed.
In the case below there are more lines, more "syntax", but the logic is in a more natural order:
<!-- language: lang-go -->
defer func() {
tempFile.Close()
os.Remove(tempFile.Name())
}()
Which one to use?
答案1
得分: 41
在这个例子中,匿名函数更容易阅读,特别是一旦你添加了错误处理。
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`通常是适当的。
<details>
<summary>英文:</summary>
In this example, the anonymous function is easier to read, especially once you add in error handling.
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)
}
}()
If you have multiple resources, then multiple `defer`s is generally appropriate.
</details>
# 答案2
**得分**: 17
根据[Ross Light][1]的回答:
如果你有多个资源,那么多个defer通常是合适的。
2019年4月:但在这种情况下,考虑使用Go 1.13(2019年第四季度),因为它集成了对[go issue 14939: "runtime: defer is slow"][3]和[go issue 6980: "cmd/compile: allocate some defers in stack frames"][4]的修复。
参见[Go CL 171758: "cmd/compile,runtime: allocate defer records on the stack"][5]
当一个defer在函数体中最多执行一次时,我们可以将其在堆栈上分配defer记录,而不是在堆上。
这应该使得像这样的defers(非常常见)更快。
这个优化适用于cmd/go二进制文件中的370个静态defer站点中的363个。
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)][6],使用了[CL 190098][7]:
defer语句的成本 [`go test -run NONE -bench BenchmarkDefer$ runtime`]
使用普通(堆栈分配)的defer:35.4 ns/op
使用开放式编码的defer:5.6 ns/op
仅函数调用的成本(去掉defer关键字):4.4 ns/op
但[Damien Grisky补充说][8]:
defer变得更便宜了,但panic/recover变得更昂贵了。
defer的成本:34ns -> 6ns。
panic/recover的成本:62ns -> 255ns
这不是一个坏的权衡。
----
换句话说,虽然使用多个defer可能是惯用的,但这种做法受到了性能成本的限制,而在Go 1.13+中不再是问题。
(正如[Paschalis][9]的博文“[What is a defer? And how many can you run?][10]”所示)
这使得在应该执行函数调用的地方(无论代码流如何)可以实现实际使用defer。
然而,[John Refior][12]指出,`defer`是同步的:
实际上,defer在函数退出之前立即执行。
它是同步的,所以调用者等待defer完成。
因此,即使现在可以有多个defer,请确保它们执行速度快,或者如John所指出的那样:
幸运的是,我们可以在`defer`中包装一个goroutine,这样就可以获得我们想要的流程控制和时间控制,而不会延迟调用者:
```go
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")
}()
}
通常,defer用于锁定互斥锁、关闭连接或文件描述符,它们的工作速度很快,或者我们希望在调用者继续之前完成它们的工作。
但是,当你在HTTP处理程序的末尾执行不需要客户端等待的慢速工作时,将调用异步化可以大大提高用户体验。
英文:
As Ross Light answer states:
> If you have multiple resources, then multiple defers is generally appropriate.
April 2019: But in that case, consider Go 1.13 (Q4 2019), as it does integrate a fix for go issue 14939: "runtime: defer is slow" and go issue 6980: "cmd/compile: allocate some defers in stack frames"
See Go CL 171758: "cmd/compile,runtime: allocate defer records on the stack"
> 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)
Oct. 2019 (Go 1.13 is released a few weeks ago)
This is confirmed (Brad Fitzpatrick) with 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
But Damien Grisky adds:
> Defer gets cheaper, but panic/recover is more expensive.
>
> Cost of defer: 34ns -> 6ns.
> Cost of panic/recover: 62ns -> 255ns
That is not a bad trade-off.
In other words, while using multiple defer can be idiomatic, that practice was held back by performance costs which are no longer a concern with Go 1.13+.
(as illustrated by Paschalis's blog post "What is a defer? And how many can you run?")
That makes practical use if defer (in places where a function call should be executed irrespective of the code flow) possible.
John Refior notes, however, that defer
is synchronous:
> Actually defer is executed immediately before the function exits.
And it occurs synchronously, so the caller waits for defer to complete.
So even if you can now have multiple defer, make sure they are fast, or, as John notes:
> Fortunately it’s easy to wrap a goroutine in a defer
, giving us the flow control and timing we want, without delaying the caller:
>
> go
> 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论