图解Go中defer
前言
defer 在 Go 语言中是一个关键字(延迟调用),一般用于释放资源和连接、关闭文件、释放锁等。本文通过多个示例,展示了 defer 的执行顺序、参数解析、与返回值的交互、以及在闭包、panic 和 os.Exit 中的特殊行为。在实际开发中,灵活运用 defer 可以简化代码逻辑,减少重复性操作,从而提升代码质量和运行效率。
为什么要有 defer
如果没有 defer
,你写的代码可能会是这样的:
r := getFileHandle() // 打开文件资源
// ......
if 错误1 {
r.close() // 释放资源
return
}
if 错误2 {
r.close()// 释放资源
return
}
有了 defer
操作后,就省心了
r := getFileHandle() // 打开文件资源
defer r.close() // 释放资源
// ......
if 错误1 {
// ...
return
}
if 错误2 {
// ...
return
}
多个defer语句,按先进后出的方式执行
package main
import "fmt"
func main () {
var test [5]int
for i := range test {
defer fmt.Println(i)
}
}
输出结果:4,3,2,1,0
defer声明时,对应的参数会实时解析
上面提到入栈的时候会进行相关的值拷贝, 那么其实就意味着对应参数会实时解析。如:
package main
import "fmt"
func main() {
i := 1
defer fmt.Print(i) // 这里会输出1,入栈的时候就实时解析了
i++
fmt.Println("i =", i)
}
// i = 2
// 1
辨析:defer
后面跟无参函数、有参函数和方法:
package main
import "fmt"
func test(a int) {//无返回值函数
defer fmt.Println("1、a =", a) //方法,a=1
defer func(v int) { fmt.Println("2、a =", v)} (a) //有参函数,a=1
defer func() { fmt.Println("3、a =", a)} () //无参函数,a=2
a++
}
func main() {
test(1)
}
// 3、a = 2
// 2、a = 1
// 1、a = 1
下面两个打印输出读者应该都能理解:
defer fmt.Println("1、a =", a) //方法,打印a=1
defer func(v int) { fmt.Println("2、a =", v)} (a) //有参函数,打印a=1
那为什么唯独 defer func() { fmt.Println("3、a =", a)} ()
这个无参函数会打印 2
呢?说好的实时解析变量,即时快照呢?
下面坐稳扶好,开始慢慢上点强度了~
defer与闭包
例子 1
package main
import "fmt"
func test(a int) {//无返回值函数
defer func() { fmt.Println("a =", a)} () //无参函数,a=2
a++
}
func main() {
test(1)
}
上面的无参函数其实是闭包来的,不知道啥是闭包的可以翻看我前面的文章。
说白了原因就是:
上面的 defer
后的闭包函数确实是提前被压入栈了,但是呢,变量 a 的值却没有提前压入栈,被提前压入栈的是 a 的引用,也就是指向 a 的指针!用个图来表示下:
例子 2
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) pp() {
fmt.Println(t.name)
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer t.pp()
}
}
一眼看到不是闭包,而是 defer
后调用方法,应该猜想依次反序输出为: c,b,a
但是却不是,结果输出为:c,c,c
为什么会这样呢?其实上面的代码我做下等价转换下,你可能就懂了:
package main
import "fmt"
type Test struct {
name string
}
func pp(t *Test) { // 注意方法我进行了等价转换,变成传指针的函数
fmt.Println(t.name)
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer pp(&t) // 注意这里传的是 t 的指针。
}
}
如下代码其实是个语法糖,他可以进行等价转换,实际上就是写的形式不一样而已。
func (t *Test) pp() {
fmt.Println(t.name)
}
// ---- 等价于下面代码 ----
func pp(t *Test) {
fmt.Println(t.name)
}
// -------举一反三-----------
func (t Test) pp() {// 方法接受者不为指针。
fmt.Println(t.name)
}
// ---- 等价于下面代码 ----
func pp(t Test) {// 参数也不为指针
fmt.Println(t.name)
}
方法接受者是指针类型,所以对应等价转换的函数,其参数值也为指针类型。否则都不为指针类型
所以呢,做了转换之后,我们可能就豁然开朗了:
原来 defer 入栈时依然是实时解析的,不过这里实时解析的变量是 t 的指针类型!
使用 range
,会将 ts
中的成员复制一份出来到新的变量 t
中,而 t
由始至终都是同一个内存地址(这里不懂的读者可以翻看我前面关于 for 循环的坑的那篇文章),意味着 pp(&t) 传递的指针都是同一个。
等到 for
结束时 t.name="c"
,意味着&t
指向的 c
值都是 "c"
, 所以接下来执行的那些 defer
语句后的函数打印都为 c
。
解决这个问题呢,可以修改代码为:
package main
import "fmt"
type Test struct {
name string
}
func (t Test) pp () { // 去掉指针
fmt.Println(t.name)
}
func pp(t Test) { // 去掉指针
fmt.Println(t.name)
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer pp(t)
// 或者下面这种方式也行
// defer t.pp()
}
}
这个时候是直接传的是 t 值,而不是指针,t 值就会被 defer 及时解析,压入栈中。
当然,也可使用临时变量的方式进行修改:
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) pp() {
fmt.Println(t.name)
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
tt := t
println(&tt)
defer tt.pp()
}
}
// 0xc000010200
// 0xc000010210
// 0xc000010220
// c
// b
// a
解释:
在for
循环内,可以看出每次 tt:=t
时,tt
的地址都不同。每次都有一个新的变量tt:=t
,所以每次在执行defer
语句时,对应的 tt
不是同一个(for
循环中实际上生成了3
个不同的tt
),所以输出的结果也不相同。
可读取函数返回值(return返回机制)
我这里直接抛出 defer
、return
、返回值三者的执行逻辑:
return
最先执行,return
负责将结果写入返回值中;
接着defer
开始执行一些收尾工作;
最后函数携带当前返回值退出,回到主函数中。这里可能和最初的返回值不相同,因为 defer
可能在 return
之后去修改返回值。
下面我们来看例子:
无名返回值:
package main
import (
"fmt"
)
func a() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i) // 输出2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 输出1
}()
return i
}
func main() {
fmt.Println("return:", a()) // 输出0
}
// defer1: 1
// defer2: 2
// return: 0
解释:
返回值由变量i
赋值,相当于 返回值= i=0
。
第二个defer
中 i++ = 1
, 第一个defer
中i++ = 2
,所以最终i
的值是2
。
但是返回值已经被赋值了,即使后续修改i也不会影响返回值。最终返回值返回,所以main
中打印0
。
有名返回值:
package main
import (
"fmt"
)
func b() (i int) {
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return i //或者直接写成return
}
func main() {
fmt.Println("return:", b())
}
// defer1: 1
// defer2: 2
// return: 2
解释:
这里已经指明了返回值就是i
,所以后续对 i
进行修改都相当于在修改返回值,所以最终函数的返回值是2
。
函数返回值为地址
package main
import (
"fmt"
)
func c() *int {
var i int
defer func() {
i++
fmt.Println("defer2:", i)
}()
defer func() {
i++
fmt.Println("defer1:", i)
}()
return &i
}
func main() {
fmt.Println("return:", *(c()))
}
//defer1: 1
//defer2: 2
//return: 2
解释:
此时的返回值是一个指针(地址),这个指针=&i
,相当于指向变量i所在的地址,两个defer
语句都对i进行了修改,那么返回值指向的地址的内容也发生了改变,所以最终的返回值是2
。
再看一个例子:
func f() (r int) {
defer func(r int) {
r = r + 5
fmt.Println("defer: ", r)
}(r)
return 1
}
func main() {
fmt.Println("return:", f())
}
// defer: 5
// return: 1
最初返回值r的值是1
,虽然defer
语句中函数的参数名也叫r
(这里我记作r’
),但传参的时候相当于r‘=r
(值传递),函数内的语句相当于r’=r‘+5
,所以返回值r
并没有被修改,最终的返回值仍是1
。
defer与panic
func panicDefer() {
panic("panic")
defer fmt.Println("defer after panic")
}
在panic语句后面的defer语句不被执行
panic: panic
goroutine 1 [running]:
main.panicDefer()
E:/godemo/testdefer.go:17 +0x39
main.main()
E:/godemo/testdefer.go:13 +0x20
Process finished with exit code 2
在panic语句前的defer语句会被执行:
func deferPanic() {
defer fmt.Println("defer before panic")
panic("panic")
}
defer before panic
panic: panic
goroutine 1 [running]:
main.deferPanic()
E:/godemo/testdefer.go:19 +0x95
main.main()
E:/godemo/testdefer.go:14 +0x20
Process finished with exit code 2
调用os.Exit时defer不会被执行
func deferExit() {
defer func() {
fmt.Println("defer")
}()
os.Exit(0)
}
当调用os.Exit()
方法退出程序时,defer
并不会被执行,上面的defer
并不会输出。
总结
本文通过多个示例,展示了 defer
的执行顺序、参数解析、与返回值的交互、以及在闭包、panic
和 os.Exit
中的特殊行为。
在实际开发中,灵活运用 defer
可以简化代码逻辑,减少重复性操作,从而提升代码质量和运行效率。