《Go语言精进之路》读书笔记 | 使用defer让函数更简洁更健壮

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go语言精进之路》读书笔记 | 使用defer让函数更简洁更健壮相关的知识,希望对你有一定的参考价值。

书籍来源:《Go语言精进之路:从新手到高手的编程思想、方法和技巧》

一边学习一边整理读书笔记,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:《Go语言精进之路》读书笔记 | 汇总_COCOgsta的博客-CSDN博客


在函数中会申请一些资源并在函数退出前释放或关闭这些资源,函数的实现需要确保这些资源在函数退出时被及时正确地释放,解决上面提到的这些问题正是Go语言引入defer的初衷。

22.1 defer的运作机制

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

在Go中,只有在函数和方法内部才能使用defer,defer将它们注册到其所在goroutine用于存放deferred函数的栈数据结构中,这些deferred函数将在执行defer的函数退出前被按后进先出(LIFO)的顺序调度执行(见图22-1)。

图22-1 deferred函数的存储与调度执行

无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,抑或出现panic,已经存储到deferred函数栈中的函数都会被调度执行。

22.2 defer的常见用法

除了释放资源这个最基本、最常见的用法之外,defer的运作机制决定了它还可以在其他一些场合发挥作用,这些用法在Go标准库中均有体现。

  1. 拦截panic

defer的第二个重要用途就是拦截panic,并按需要对panic进行处理,可以尝试从panic中恢复(这也是Go语言中唯一的从panic中恢复的手段),也可以如下面标准库代码中这样触发一个新panic,但为新panic传一个新的error值:

// $GOROOT/src/bytes/buffer.go
func makeSlice(n int) []byte 
    // If the make fails, give a known error.
    defer func() 
        if recover() != nil 
            panic(ErrTooLarge) // 触发一个新panic
        
    ()
    return make([]byte, n)

复制代码
  1. 修改函数的具名返回值
// chapter4/sources/deferred_func_5.go

func foo(a, b int) (x, y int) 
    defer func() 
        x = x * 5
        y = y * 10
    ()

    x = a + 5
    y = b + 6
    return


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

复制代码

运行这个程序:

$ go run deferred_func_5.go
x= 30 y= 80
复制代码

我们看到deferred函数在foo真正将执行权返回给main函数之前,将foo的两个返回值x和y分别放大了5倍和10倍。

  1. 输出调试信息

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

更为典型的莫过于在出入函数时打印留痕日志(一般在调试日志级别下):

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 main() 
    b()

复制代码

运行该示例,我们将得到如下结果:

entering: b
in b
entering: a
in a
leaving: a
leaving: b
复制代码
  1. 还原变量旧值

defer还有一种比较小众的用法:

// $GOROOT/src/syscall/fs_nacl.go
func init() 
    oldFsinit := fsinit
    defer func()  fsinit = oldFsinit ()
    fsinit = func() 
    Mkdir("/dev", 0555)
    Mkdir("/tmp", 0777)
    mkdev("/dev/null", 0666, openNull)
    mkdev("/dev/random", 0444, openRandom)
    mkdev("/dev/urandom", 0444, openRandom)
    mkdev("/dev/zero", 0666, openZero)
    chdirEnv()

复制代码

这段代码的作者利用了deferred函数对变量的旧值进行还原:先将fsinit存储在局部变量oldFsinit中,然后在deferred函数中将fsinit的值重新置为存储在oldFsinit中的旧值。

22.3 关于defer的几个关键问题

“工欲善其事,必先利其器”,要想用defer,就需要提前了解几个关于defer的关键问题,以避免掉进一些不必要的“坑”。

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

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

内置函数是否都能作为deferred函数呢?append、cap、len、make、new等内置函数是不可以直接作为deferred函数的,而close、copy、delete、print、recover等可以。

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

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

  1. 知晓defer带来的性能损耗

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

在Go 1.13中,Go核心团队对defer性能做了大幅优化,官方声称在大多数情况下,defer性能提升了30%。但笔者的实测结果是defer性能的确有提升,但远没有达到30%。而在Go 1.14版本中,defer性能提升巨大,已经和不用defer的性能相差很小了:

// Go 1.14
$go test -bench . defer_perf_benchmark_1_test.go
goos: darwin
goarch: amd64
BenchmarkFooWithDefer-8       161184176          7.47 ns/op
BenchmarkFooWithoutDefer-8    228358987          5.27 ns/op
PASS
ok    command-line-arguments 3.697s
复制代码

 

以上是关于《Go语言精进之路》读书笔记 | 使用defer让函数更简洁更健壮的主要内容,如果未能解决你的问题,请参考以下文章

《Go语言精进之路》读书笔记 | 理解Go语言的设计哲学

《Go语言精进之路》读书笔记 | 汇总

《Go语言精进之路》读书笔记 | 使用一致的变量声明形式

《Go语言精进之路》读书笔记 | 使用无类型常量简化代码

《Go语言精进之路》读书笔记 | 使用iota实现枚举常量

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码