Golang 中 defer 的用法
defer 关键字
defer 类似其他 OOP 语言中的 finally 关键字。
defer 的运作机制
defer 只能运行在函数或者方法内部,如下:
func foo() {
defer func() {
fmt.Println("exec defer func")
}()
}
func(p *P)String() {
defer func() {
fmt.Println("exec defer func")
}()
}
defer 关键字后面只能接函数或方法,这些函数被称为 deferred 函数(例如上面的匿名 func)。defer 将它们注册到其所在 goroutine 用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前被按后进先出(LIFO)的顺序调度执行。
直观代码展示:
func foo() {
for i :=0; i<=3; i++ {
defer func(n int){
fmt.Println("n: ", n)
}(i)
}
}
以上代码的入栈顺序结果为:
func(0)
func(1)
func(2)
func(3)
// output:
// n: 3
// n: 2
// n: 1
// n: 0
// Last In First Out
defer 的常用方法
1.deferred func + return
当一个函数被 deferred 后,该函数中 return 的内容会被丢弃
func foo() (a, b int) {
fmt.Println("exec foo")
// 函数 foo 被 deferred 后 return 语句会被丢弃
return 1, 2
}
func bar() (x, y int) {
defer foo()
return 1, 2
}
func TestFooBar(t *testing.T) {
fmt.Println("test foo bar")
x, y := bar()
fmt.Println("x:", x, "y:", y)
}
2.defer 释放资源
defer 的一个主要用途就是释放资源,其中 mu.Lock() 与 defer mu.Unlock() 是成对出现的,这样成对出现的惯例极大降低了遗漏资源释放的可能性。
func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(fname, os.O_RDWR, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(0, 2)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
return f.Sync()
}
3.defer + panic
defer 的第二个重要用途就是拦截 panic,并按需要对 panic 进行处理,可以尝试从 panic 中恢复(这也是 Go 语言中唯一的从 panic 中恢复的手段)
func genPanic() {
fmt.Println("gen a panic")
panic(-1)
}
func panicFunc() {
defer func() {
if recover() != nil {
fmt.Println("catch panic")
}
}()
genPanic()
}
func TestPanicFunc(t *testing.T) {
fmt.Println("exec start")
panicFunc()
fmt.Println("exit normally")
}
// output:
//exec start
//gen a panic
//catch panic
//exit normally
//--- PASS: TestPanicFunc (0.00s)
WARNING
特别说明:deferred 函数虽然可以拦截绝大部分的 panic,但无法拦截并恢复一些运行时之外的致命问题。比如通过 C 代码“制造”的崩溃,deferred 函数便无能为力的。
4.修改具名返回值
func genReturns(i, j int) (x, y int) {
defer func() {
x = x * 5
y = y * 10
}()
x = i + 1
y = j + 2
return
}
func TestReturns(t *testing.T) {
x, y := genReturns(1, 2)
fmt.Println("x: ", x, "y: ", y)
}
// output
//x: 10 y: 40
//--- PASS: TestReturns (0.00s)
解释一下上面的步骤:
// 1. deferred 匿名函数入栈
// x = i + 1 => x = 2
// y = j + 2 => y = 3
// 即 return 时 x = 2, y = 3
// 真正的退出函数之前,deferred 匿名函数开始执行 x = 2 * 5 , y = 4 * 10
// 最后 return x, y 返回值
5.defer + 调试日志
defer 可以实现在出入函数的时候打印调试级别的日志。
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func TestTraceLog(t *testing.T) {
b()
}
// output
// entering: b
// in b
// entering: a
// in a
// leaving: a
// leaving: b
golang 中可以被 deferred 的函数
对于自定义的函数或方法,defer 可以给予无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。 Go 语言中内置的函数列表:
append cap close complex copy delete imag len
make new panic print println real recover
以上内置函数中可以被 deferred 的只有以下几种:
close copy delete print recover
但如果说你希望其余的内置函数同样可以被 deferred,你可以将该内置函数封装成一个匿名函数:
defer func() {
_ = append(s1, 11)
}
注意 defer 的求值时机
defer 关键字后面的表达式是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。
func foo1() {
for i := 0; i <= 3; i++ {
defer fmt.Println(i)
}
}
func foo2() {
for i := 0; i <= 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i)
}
}
func foo3() {
for i := 0; i <= 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
func main() {
fmt.Println("foo1 result:")
foo1()
fmt.Println("\nfoo2 result:")
foo2()
fmt.Println("\nfoo3 result:")
foo3()
}
先看输出日志,然后逐个分析 foo1、foo2、foo3 这三个函数的输出:
// output
foo1 result:
3
2
1
0
foo2 result:
3
2
1
0
foo3 result:
4
4
4
4
foo1 函数 for 循环在执行的时候 defer 注册函数入栈,依次注册了
fmt.Println(0)
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
// LIFO 所以会打印 3 2 1 0
foo2 函数 for 循环在执行的时候注册了匿名的 deferred 函数,依次注册了
func(n int) {
fmt.Println(n)
}(0)
func(n int) {
fmt.Println(n)
}(1)
func(n int) {
fmt.Println(n)
}(2)
func(n int) {
fmt.Println(n)
}(3)
// LIFO 所以会打印 3 2 1 0
foo3 函数 for 循环在执行的时候同样注册了 3 个匿名的 deferred 函数,不过是没有带参数的,所以依次注册了
defer func() {
fmt.Println(i)
}()
// 匿名函数以闭包的方式访问外围函数的变量i,并通过Println输出i的值,此时i的值为4
// LIFO 所以会一次打印 4 4 4 4
再来看一个例子
func foo4() {
sl := []int{1, 2, 3}
defer func(a []int) {
fmt.Println(a)
}(sl)
sl = []int{3, 2, 1}
_ = sl
}
func foo5() {
sl := []int{1, 2, 3}
defer func(p *[]int) {
fmt.Println(*p)
}(&sl)
sl = []int{3, 2, 1}
_ = sl
}
func TestFunc2(t *testing.T) {
foo4()
foo5()
}
// output
// [1 2 3]
// [3 2 1]
foo4 在执行 defer 的时候注册了匿名函数
defer func(a []int) {
fmt.Println(a)
}([] int{1, 2, 3})
// 即匿名函数在注册时 a = []int{1, 2, 3}
此时 deferred 函数的入参已经确定为 []int{1, 2, 3}
,所以最后就算是 sl 的值重新赋值了,但并不影响到 deferred 函数的入参。 foo5 在执行 defer 的时候注册的匿名函数,其参数是一个地址,所以在后面 sl 被重新赋值时,由于地址是不变的,所以也就输出了[3, 2, 1]
参考资料
- 《Go 语言精进之路:卷一》-白明
- go-defer 的用法及执行原理_爱吃烤面筋的鱼的博客-CSDN 博客