Go进阶defer关键字

0 阅读6分钟

Go并发编程与诗词内容整合图.png

1.defer运作机制:

defer的运作离不开函数.至少有两层含义:

1).在Go中.只有在函数和方法内部才能使用defer.

2).defer关键字后面只能接函数或方法.这些函数被称为deferred函数.defer将它们注册到其所在的goroutine用于存放在deferred函数的栈数据结构中.这些deferred函数将在执行defer的函数退出前被按后进先出(LIFO)的顺序调度执行.

无论是执行到函数体尾部返回.还是在某个错误处理分支显示调用return返回.抑或出现panic.已经存储到deferred函数栈中的函数都会被调度执行.因此.deferred函数是一个在任何情况下都可以为函数进行收尾工作的好场合.

2.defer常见用法:

1).拦截panic:

defer的运行机制决定了无论函数是执行到函数体末尾正常返回.还是在函数体中的某个错误分支显示调用return返回.抑或函数体内出现panic.已经注册了的deferred函数都会被调度执行.因此.defer的第二个重要用途就是拦截panic.并按需要对panic进行处理.可以尝试从panic中恢复(这是Go语言中唯一的从panic中恢复的手段).

示例:

func main() {
    foo()
    fmt.Println("main exit normally")
}

func foo() {
    defer func() {
       if e := recover(); e != nil {
          fmt.Println("recovered from a panic!")
       }
    }()
    bar()
}

func bar() {
    fmt.Println("raise a panic!")
    panic(-1)
}

执行结果:

2).修改函数的具名返回值:

示例:

func main() {
    x, y := foo(1, 2)
    fmt.Println("x=", x, "y=", y)
}

func foo(a, b int) (x, y int) {
    defer func() {
       x = x * 5
       y = y * 10
    }()
    x = a + 5
    y = b + 6
    return
}

执行结果:

3).输出调试信息:

deferred函数被注册及调度执行的时间点使得它们十分适合用来输出一些调试信息.

示例:

func main() {
    b()
}

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()
}

执行结果:

4).还原变量旧值:

示例:

func main() {

    //初始值.
    var value int = 10
    val := value
    defer func() {
       value = val
       fmt.Println("value:", value)
    }()
    fmt.Println(" old value:", value)
    value = 20
    fmt.Println("new value:", value)
    fmt.Println("-----------------")
}

执行结果:

3.defer关键问题:

1).明确哪些函数可以作为deferred函数:

对于自定义的函数或方法.defer可以给予无条件支持.但是对于有返回值的自定义函数或方法.返回值会在deferred函数被调度执行的时候自动丢弃.

Go语言中除了有自定义的函数或方法.还有内置函数.内置函数如下.

append cap close complex copy delete imag len make new panic print println real recover.

内置函数是否都可以作为deferred函数呢.

示例:

func main() {
    foo()
}

func bar() (int, int) {
    return 1, 2
}

func foo() {
    var c chan int
    var sl []int
    var m = make(map[string]int, 10)
    m["item1"] = 1
    m["item2"] = 2

    var a = complex(1.0, -1.4)
    var s11 []int

    defer bar()
    defer append(sl, 11)
    defer cap(sl)
    defer close(c)
    defer complex(2, -2)
    defer copy(s11, sl)
    defer delete(m, "item1")
    defer imag(a)
    defer len(sl)
    defer make([]int, 10)
    defer new(*int)
    defer panic(1)
    defer print("hello world")
    defer println("hello world")
    defer real(a)
    defer recover()
}

执行结果:

Go编译器给出了一组错误提示.从中我们可以看到append cap len make new等内置函数是不可以直接作为deferred函数的.close copy delete print recover可以.

2).把握好defer关键字后表达式的求值时机:

牢记一点.defer关键字后面的表达式是在将deferred函数注册到deferred函数栈的时候进行求值的.

示例:

func main() {
    fmt.Println("foo1 result:")
    foo1()
    fmt.Println("\nfoo2 result:")
    foo2()
    fmt.Println("\nfoo3 result:")
    foo3()
}

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(i)
       }(i)
    }
}

func foo3() {
    for i := 0; i <= 3; i++ {
       defer func() {
          fmt.Println(i)
       }()
    }
}

执行结果:

(我的Go版本是1.24.0.闭包的坑已经修复了.)

在foo1中.defer后面直接接的是fmt.Println函数.每当defer将fmt.Println注册到deferred函数栈的时候.都会对Println后面的参数进行求值..根据上面代码.依次压入deferred函数栈的函数是:

fmt.Println(0)

fmt.Println(1)

fmt.Println(2)

fmt.Println(3)

在foo1返回后.deferred函数被调度执行时.上述压入栈的deferred函数将以LIFO次序出栈执行.因此输出结构为.

3 2 1 0

在foo2中.defer后面接的是一个带有一个参数的匿名函数.每当defer将匿名函数注册到deferred函数栈的时候.都会对该匿名函数的参数进行求值.根据上述代码逻辑.依次压入deferred的函数栈的函数是.

func(0)

func(1)

func(2)

func(3)

因此在.在foo2返回后.deferred函数被调度执行时.上述压入栈的deferred函数将以LIFO次序出栈执行.输出结果为

3 2 1 0.

在foo3中.defer后面接的是一个不带参数的匿名函数.根据上面代码逻辑.依次压入deferred函数栈的函数是.

func()

func()

func()

func()

因此.在foo3返回后.deferred函数被调度执行时.上次压入栈的deferred函数将以LIFO次序出栈运行.匿名函数以闭包的方式访问外围函数的变量i.并通过Println输出i的值.此时i的值为4.因此foo3的输出结果为:

4 4 4 4

示例2:

func main() {
    foo1()
    foo2()

}

func foo1() {
    s1 := []int{1, 2, 3}
    defer func(a []int) {
       fmt.Println(a)
    }(s1)

    s1 = []int{3, 2, 1}
    _ = s1
}

func foo2() {
    s1 := []int{1, 2, 3}
    defer func(p *[]int) {
       fmt.Println(*p)
    }(&s1)
    s1 = []int{3, 2, 1}
    _ = s1
}

执行结果:

在foo1中.defer后面的匿名函数接受一个切片类型参数.当defer将该匿名函数注册到deferred函数栈的时候.会对它进行求值.此时传入的变量s1的值为[]int{1,2,3}.因此压入deferred函数栈的函数是:

func([]int{1,2,3})

之后虽然s1被重新赋值.但是在foo1返回之后.deferred函数被调度执行.deferred函数的参数值依然为[]int{1,2,3},因此输出的结果为[1,2,3]

在foo2中.defer后面的匿名函数接收一个切片指针类型的参数.当defer将该匿名函数注册到deferred函数栈的时候.会对它参数进行求值.此时传入参数的变量s1的地址.因此压入deferred的函数栈的函数是.

func(&s1)

之后虽然s1被重新赋值.但是在foo2返回后.deferred函数被调度执行.deferred函数的参数值依然为s1地址.而此时s1的值已经变为[]int{3,2,1}.因此foo2输出结果为[3,2,1].

3).知晓defer带来的性能损耗:

defer进行资源释放(如文件描述符 锁)的过程变得优雅很多.也不易出错.但在性能敏感的程序中.defer带来的性能负担也是必须知晓和权衡的.

示例:

func sum(max int) int {
    total := 0
    for i := 0; i < max; i++ {
       total += i
    }
    return total
}

func fooWithDefer() {
    defer func() {
       sum(10)
    }()
}

func fooWithoutDefer() {
    sum(10)
}

func BenchmarkFooWithDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
       fooWithDefer()
    }
}

func BenchmarkFooWithoutDefer(b *testing.B) {
    for i := 0; i < b.N; i++ {
       fooWithoutDefer()
    }
}

执行结果:

withDefer:

withoutDefer:

密洒征鞍无数。冥迷远树。乱山重叠杳难分,似五里、蒙蒙雾。

惆怅琐窗深处。湿花轻絮。当时悠飏得人怜,也都是、浓香助。纳兰

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路