图解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栈区

辨析: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 的指针!用个图来表示下:

defer和闭包

例子 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返回机制)

我这里直接抛出 deferreturn、返回值三者的执行逻辑:

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

第二个deferi++ = 1, 第一个deferi++ = 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 的执行顺序、参数解析、与返回值的交互、以及在闭包、panicos.Exit 中的特殊行为。

在实际开发中,灵活运用 defer 可以简化代码逻辑,减少重复性操作,从而提升代码质量和运行效率。

关于我
loading