go语言中的延迟执行函数

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 【5月更文挑战第13天】`defer`是Go语言中用于延迟执行函数的关键字,尤其适用于资源管理,如文件关闭和锁的释放。它在函数返回前按照LIFO顺序执行,确保资源在任何返回路径下都能正确释放。`defer`可以拦截`panic`并在函数返回前执行,但无法阻止某些致命的`panic`。此外,`defer`可用于修改返回值、输出调试信息和还原变量值。尽管在某些场景下可能影响性能,但Go的优化使得其在多数情况下性能表现良好,特别是在资源清理方面。在Go 1.20及以后的版本,`defer`的性能已显著提升,尤其是在高计算量的场景下。

1 延迟执行函数简介

在go1.13及以前,defer 性能比 直接调用差,执行性能至少差5倍。

在版本1.20,当函数需要调用和执行次数更多,调用defer 的延迟计算与直接调用性能更好,这说明go的调度算法更高效了。

defer 在return 之后调用,但是在返回给调用方之前修改,可以用于修改return 返回值。

适用的场景包括 比如文件读写,互斥锁的释放,函数的实现需要确保这些资源在函数退出时被及时正确释放,

无论函数执行流顺序执行还是提前退出。 开发人员需要对函数的错误处理尤为关注,在错误处理时不能遗留资源的释放问题。

尤其是多个资源需要释放时。 此外,当待释放资源个数较多时,代码逻辑将变得十分复杂,程序可读性,健壮性也下降。

即便如此,如果函数实现的某个代码逻辑抛出panic,传统错误处理机制依然没有办法捕获它并从panic恢复。

这就是defer的适用场景。

  • defer只能在函数中使用,并且只能接函数或方法。

    这些函数被称为延迟函数 deferred。

    defer将他们注册到所在 goroutine 用于存放 deferred 函数的栈数据中,这些 deferred函数将在 所在函数退出前执行

    其执行顺序为 先进后出 LIFO。

    无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,逆或出现panic,
    已经存储到 deferred 函数栈的将被调用执行。 因此,deferred 函数是一个在任何情况下都可以为函数进行 收尾工作的工具。

    协程 goroutine                  deferred 函数栈
                                      func3
    func foo() {     ---注册入栈-->    func2        
      defer func1                     func1
      defer func2
      ...                              | 
    }                                  | 调度出栈 LIFO
                                       |

                                    go runtime 运行时
                                      func3
                                      func2
                                      func1

例子:

  func writeFile(fname string, data []byte, mu * sync.Mutex) error {
        mu.Lock()
        defer mu.Unlock()

        f, err := os.OpenFile(fname, os.O_RDWR, 0666)
        ...

        defer f.Close()

        f.Seek(0, 2)
        ...
        f.Write(data)
        ...

        return f.Sync()
}

此关键字的使用对函数 写文件的实现逻辑 简化是显而易见的,资源释放函数 defer 注册动作紧邻资源申请成功的动作。

这样成对出现的惯例极大降低了遗落资源释放的可能性,开发者不用小心翼翼在每个错误处理分支检查是否遗漏资源释放操作。

代码简化又增加了可读性和健壮性。

2 常见用法

  • 拦截panic

    当panic出现时,已经注册的defer函数将在函数返回前执行。 也可以从panic其中恢复。

    或者另外触发一个panic返回新error。

    func errorOld() {

     fmt.Println("raise a panic.")
     panic(-1)
    

    }

    func ErrorNew() {

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

    }

    deferred 函数在出现panic 时依旧可以被调度执行,这一特征让两个看似类似的行为等价函数在触发panic时返回不同结果。

    defer 可以拦截大部分panic,但是runtime 运行时的部分致命问题无法拦截。 比如一些 cgo 或者汇编代码造成的问题。

  • 修改函数返回值。

    这个已经被反复说过,就不再举例。

  • 输出调试信息

    defer函数 被注册和调度执行时,使它十分适合输出一些调试信息,比如 Go 标准库的net包的hostLookupOrder 方法就使用deferred 在特定日志级别输出一些日志以便程序调试和跟踪。

    更典型的莫过于在函数的出入位置打印留痕信息,一般是调试日志级别。
    比如Go官方的文档

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

    func b() {
    defer un(trace("b"))
    fmt.Println("in b func")
    a()
    }

    func main() {
    b()
    }

    执行信息:

    entering: b
    in b func
    entering: a
    in a
    leaving:a
    leaving:b

  • 还原变量旧值

    defer 还有一个比较小众的用法,依旧来自Go标准库,在syscall有这样的代码

    //src/syscall/fs_nacl.go
    func init() {

     oldFsinit := fsinit
     defer func() {
     fsinit = oldFirst
    

    }()

    fsinit = func() {}
    Mkdir("/dev", 0555)
    Mkdir("/tmp", 0777)
    Mkdir("/dev/null", 0666, openNull)
    Mkdir("/dev/random", 0444, openRandom)
    Mkdir("/dev/urandom", 0444, openRandom)
    Mkdir("/dev/zero", 0666, openZero)
    chdirEnv()
    }

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

3 性能问题,defer的使用限制

自定义函数,defer 可以任意使用。

一些内置函数 如下:

  append, cap, close, complex, copy, delete, image, len, make, new, panic, print, println, real, recover

除了这五个内置函数可以直接进行defer延迟调用: close, copy, delete, print, recover,

对于其他的不能使用 defer直接调用,可以使用匿名函数调用其他的内置函数。

  defer func() {
      _ = append(s, 11)
  }()

同时需要注意 defer 关键字后面的表达式将 deferred 函数注册到 deferred函数栈的时候求值。

defer的调用和性能对比例子:

使用基准性能函数执行测试

      func BenchmarkDeferFuncInner(b * testing.B) {
            for n := 0; n < b.N; n++ {
                deferFuncInner()
            }
        }

执行结果:

      goarch: amd64
    cpu: AMD Ryzen 5 3500U

    BenchmarkFuncInnerSlice-8                 45641259            26.59 ns/op
    BenchmarkDeferFuncInner-8                 63858306            36.48 ns/op

    BenchmarkFuncInnerCap-8                   1000000000             0.4553 ns/op
    BenchmarkDeferFuncInnerCap-8              238949815             5.044 ns/op

    BenchmarkFuncInnerChan-8                   8231943           128.9 ns/op
    BenchmarkDeferFuncInnerChan-8              9563726           135.3 ns/op

    BenchmarkFuncInnerComples-8               1000000000             0.4856 ns/op
    BenchmarkDeferFuncInnerComples-8          228120228             5.095 ns/op

    BenchmarkFuncInnerCopy-8                  1000000000             0.4460 ns/op
    BenchmarkDeferFuncInnerCopy-8             80005333            12.83 ns/op

    BenchmarkFuncInnerDelete-8                13218952            97.63 ns/op
    BenchmarkDeferFuncInnerDelete-8           11846703            89.90 ns/op

    BenchmarkFuncInnerImage-8                 1000000000             0.5341 ns/op
    BenchmarkDeferFuncInnerImage-8            224122573             5.742 ns/op

    BenchmarkFuncInnerLenAndMake-8            1000000000             0.5217 ns/op
    BenchmarkDeferFuncInnerLenAndMake-8       219836587             5.315 ns/op

    BenchmarkFuncInnerNew-8                   1000000000             0.5438 ns/op
    BenchmarkDeferFuncInnerNew-8              158230893             6.546 ns/op

    BenchmarkFuncInnerPrintf-8              23668                59549 ns/op  
    BenchmarkDeferFuncInnerPrintf-8          20163               84129 ns/op

    BenchmarkDeferFuncInnerRecover-8           3195561           365.2 ns/op
    PASS
    ok      command-line-arguments    31.431s

大部分闭包场景 defer的性能比直接调用差。
某些场景比如 defer delete 可能比直接调用性能好。

延迟调用在释放资源时,比如文件描述符,锁,释放的过程更优雅。

我们再进行高并发性能对比:

明显的例子: 性能成本评估

  package main

  import "testing"

  var (
    n = 100
  )

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

  func sumWithDefer() {
    defer func() {
      sum(n)
    }()
  }

  func sumDirect() {
    sum(n)
  }

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

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

n = 100 表示自定义函数只在直接调用和延迟调用中分别 执行 100次计算。

执行结果:

    go test -v -count 2 -bench .  defer_bench_test.go   -cpu 2,4,8,32,128 >bmdefer.txt

goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx  
BenchmarkSumWithDefer
BenchmarkSumWithDefer-2         11537151          90.68 ns/op
BenchmarkSumWithDefer-2         15462482          85.18 ns/op
BenchmarkSumWithDefer-4         13944864          84.05 ns/op
BenchmarkSumWithDefer-4         14267035          77.33 ns/op
BenchmarkSumWithDefer-8         12361384         102.1 ns/op
BenchmarkSumWithDefer-8         15771418          88.03 ns/op
BenchmarkSumWithDefer-32        17111298          82.18 ns/op
BenchmarkSumWithDefer-32        17136492          73.74 ns/op
BenchmarkSumWithDefer-128       17141877          77.62 ns/op
BenchmarkSumWithDefer-128       15431292          80.83 ns/op
BenchmarkSumDirect
BenchmarkSumDirect-2             9422347         125.2 ns/op
BenchmarkSumDirect-2             9486440         114.1 ns/op
BenchmarkSumDirect-4            11385836         120.0 ns/op
BenchmarkSumDirect-4            11305587         119.3 ns/op
BenchmarkSumDirect-8             9186848         120.3 ns/op
BenchmarkSumDirect-8             9998116         127.3 ns/op
BenchmarkSumDirect-32           11427699         117.2 ns/op
BenchmarkSumDirect-32           10809895         116.4 ns/op
BenchmarkSumDirect-128           8740372         124.0 ns/op
BenchmarkSumDirect-128           8668395         136.8 ns/op
PASS
ok    command-line-arguments  31.975s

这里的延迟调用 defer的执行性能并没有比直接执行更差,基本持平,并且甚至更好。

再次执行,这一次自定义函数只在直接调用和延迟调用中分别 执行 10次

    goarch: amd64
    cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx  
    BenchmarkSumWithDefer
    BenchmarkSumWithDefer-2         100000000         11.03 ns/op
    BenchmarkSumWithDefer-2         100000000         11.84 ns/op
    BenchmarkSumWithDefer-4         100000000         11.01 ns/op
    BenchmarkSumWithDefer-4         100000000         11.38 ns/op
    BenchmarkSumWithDefer-8         96319328          11.14 ns/op
    BenchmarkSumWithDefer-8         100000000         12.97 ns/op
    BenchmarkSumWithDefer-32        100750069         11.51 ns/op
    BenchmarkSumWithDefer-32        113154549         11.28 ns/op
    BenchmarkSumWithDefer-128       100000000         15.15 ns/op
    BenchmarkSumWithDefer-128       100978339         11.63 ns/op
    BenchmarkSumDirect
    BenchmarkSumDirect-2            92170146          11.73 ns/op
    BenchmarkSumDirect-2            100000000         14.49 ns/op
    BenchmarkSumDirect-4            100039532         11.44 ns/op
    BenchmarkSumDirect-4            105437055         11.23 ns/op
    BenchmarkSumDirect-8            100000000         10.67 ns/op
    BenchmarkSumDirect-8            100000000         11.75 ns/op
    BenchmarkSumDirect-32           85713673          13.80 ns/op
    BenchmarkSumDirect-32           104137286         11.33 ns/op
    BenchmarkSumDirect-128          100000000         12.43 ns/op
    BenchmarkSumDirect-128          100000000         12.11 ns/op
    PASS
    ok    command-line-arguments  30.810s

性能与直接调用基本持平。

4 小结

defer是Go语言中用于延迟执行函数的关键字,尤其适用于资源管理,如文件关闭和锁的释放。它在函数返回前按照LIFO顺序执行,确保资源在任何返回路径下都能正确释放。
defer可以拦截panic并在函数返回前执行,但无法阻止某些致命的panic。此外,defer可用于修改返回值、输出调试信息和还原变量值。

尽管在某些场景下可能影响性能,但Go的优化使得其在多数情况下性能表现良好,特别是在资源清理方面。在Go 1.20及以后的版本,defer的性能已显著提升,尤其是在高计算量的场景下。

多数场景,我们的程序对性能并不敏感,但是即使敏感的场景,deferred函数也可以很好的完成执行。

它的注册和调度执行已经得到较大优化。

        * 自定义函数, 低计算时长场景,defer性能与直接调用 自定义函数性能相当。
        * 自定义函数, 高计算时长场景,defer性能 比 直接调用 更好  。

        * 内建函数,大部分场景(除delete)
         defer调用内建函数的 性能 比 直接调用内建函数 更差  。
目录
相关文章
|
1天前
|
Go 开发者
探索Go语言的并发编程模型
通过实例详细介绍了Go语言中的并发编程模型,包括goroutine、channel的基本使用和最佳实践。深入剖析如何利用Go的并发特性提高程序性能和效率,适用于初学者和有一定经验的开发者。
|
3天前
|
Go Python
go语言调用python脚本
go语言调用python脚本
7 0
|
5天前
|
负载均衡 算法 Java
【面试宝藏】Go语言运行时机制面试题
探索Go语言运行时,了解goroutine的轻量级并发及GMP模型,包括G(协程)、M(线程)和P(处理器)。GMP调度涉及Work Stealing和Hand Off机制,实现负载均衡。文章还讨论了从协作到基于信号的抢占式调度,以及GC的三色标记算法和写屏障技术。理解这些概念有助于优化Go程序性能。
23 4
|
6天前
|
JSON Go 数据格式
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(4)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
Java 编译器 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
存储 安全 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(2)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
Java Go 索引
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6天前
|
安全 Go 开发者
Go语言中的空值与零值有什么区别?
在Go语言中,`nil`和零值有显著区别。`nil`用于表示指针、通道等类型的“无”或“不存在”,而零值是类型的默认值,如数字的0,字符串的`&#39;&#39;`。`nil`常用于未初始化的变量或错误处理,零值用于提供初始值和避免未初始化的使用。理解两者差异能提升代码质量和稳定性。
|
8天前
|
Go
如何理解Go语言中的值接收者和指针接收者?
Go语言中,函数和方法可使用值或指针接收者。值接收者是参数副本,内部修改不影响原值,如示例中`ChangeValue`无法改变`MyStruct`的`Value`。指针接收者则允许修改原值,因为传递的是内存地址。选择接收者类型应基于是否需要修改参数,值接收者用于防止修改,指针接收者用于允许修改。理解这一区别对编写高效Go代码至关重要。
|
9天前
|
缓存 Java Go
如何用Go语言构建高性能服务
【6月更文挑战第8天】Go语言凭借其并发能力和简洁语法,成为构建高性能服务的首选。本文关注使用Go语言的关键设计原则(简洁、并发、错误处理和资源管理)、性能优化技巧(减少内存分配、使用缓存、避免锁竞争、优化数据结构和利用并发模式)以及代码示例,展示如何构建HTTP服务器。通过遵循这些原则和技巧,可创建出稳定、高效的Go服务。