深入理解Go语言defer:不仅是语法糖

本文深入探讨Go语言中defer关键字的语义特性,通过实际代码示例展示defer在异常处理中的重要作用,解释为什么defer不仅仅是语法糖,而是确保代码正确性的关键工具。

Defer:甜美但不仅仅是语法糖

简而言之的defer

在学习Go语言时,开发者很快就会遇到defer关键字。例如,《Go语言之旅》这样介绍defer:

defer语句将函数的执行推迟到周围函数返回时。延迟调用的参数会立即求值,但函数调用要等到周围函数返回才会执行。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}

defer提供了一种便捷的方式来确保某些代码在封闭函数将控制权返回给调用者之前无条件执行。

通常,你会编写一个获取某些资源(文件描述符、互斥锁等)的函数。这样的函数通常必须在返回前释放这些资源,无论执行路径如何——你的函数可能确实包含多个return语句。如果不这样做,很可能在运行时导致问题,例如资源泄漏、死锁等。

《Go语言之旅》并非对Golang的详尽介绍;它在演示defer在封闭函数正常返回时的工作原理方面做得很好,但遗漏了一些重要的微妙之处。

你对defer的理解有多好?

测验时间!

为了测试Gopher对defer语义的理解,我在Twitter上进行了以下投票:

函数foo和bar具有完全相同的行为/语义:

1
2
3
4
5
6
7
8
9
func foo(f func()) {
    defer fmt.Println("bye")
    f()
}

func bar(f func()) {
    f()
    fmt.Println("bye")
}

真还是假?

花点时间思考一下……你的答案会是什么?

尽管投票的回应率很低(只有16位受访者),而且受访者的Golang熟练程度难以确定,但我确实发现结果很有启发性:大多数受访者(56%)回答"真",但正确答案是"假"。

解释

函数foo和bar不具有相同的语义。具体来说,如果函数f在被调用时发生panic——要么是因为它恰好是nil,要么是因为在其执行期间发生了panic——函数bar将简单地panic(不打印任何内容),而函数foo将在panic之前向标准输出打印"bye"。

你可以在Go Playground上亲自尝试。在这种情况下,如果要无条件执行fmt.Println("bye"),使用defer不是可选的,因为函数bar无法保证其调用者传递的函数参数在被调用时不会panic。

得益于defer提供的针对panic的保证,函数foo完全避免了这个问题。

使用defer还是不使用defer:这是个问题

这个例子应该作为一个警示。defer容易被误解(有罪!)为仅仅是语法上的便利。不使用它可能会导致你的Go程序出现严重问题。

诚然,如果你正在编写性能关键的代码(如数据库)并且你知道自己在做什么,你可能不得不在代码的某些地方避免使用deferred语句,但是

总是使用defer(除非你有充分的理由不这样做)

是一个很好的经验法则。

引用Bill Kennedy(Twitter上的goinggodotnet)的劝诫,他在视频课程中经常重复:

正确性优先于性能。

牢固掌握defer的语义很可能为你的Go程序的正确性带来巨大的回报。

不要推迟阅读关于defer的细则 :)

附加资源

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计