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 语言当中 CHANNEL 缓冲
Go 语言当中 CHANNEL 缓冲
|
2天前
|
中间件 Go
go语言后端开发学习(三)——基于validator包实现接口校验
go语言后端开发学习(三)——基于validator包实现接口校验
|
2天前
|
存储 Go 开发工具
go语言后端开发学习(二)——基于七牛云实现的资源上传模块
go语言后端开发学习(二)——基于七牛云实现的资源上传模块
|
2天前
|
JSON 算法 Go
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
|
2天前
|
Go 数据库
Go语言之GORM框架(四)——预加载,关联标签与多态关联,自定义数据类型与事务(完结篇)
Go语言之GORM框架(四)——预加载,关联标签与多态关联,自定义数据类型与事务(完结篇)
|
2天前
|
SQL Go
Go语言之GORM框架(三)——Hook(钩子)与Gorm的高级查询
Go语言之GORM框架(三)——Hook(钩子)与Gorm的高级查询
|
2天前
|
SQL Go 数据库
Go语言之GORM框架(二) ——GORM的单表操作
Go语言之GORM框架(二) ——GORM的单表操作
|
1月前
|
开发框架 安全 中间件
Go语言开发小技巧&易错点100例(十二)
Go语言开发小技巧&易错点100例(十二)
38 1
|
1月前
|
缓存 负载均衡 网络协议
使用Go语言开发高性能服务的深度解析
【5月更文挑战第21天】本文深入探讨了使用Go语言开发高性能服务的技巧,强调了Go的并发性能、内存管理和网络编程优势。关键点包括:1) 利用goroutine和channel进行并发处理,通过goroutine池优化资源;2) 注意内存管理,减少不必要的分配和释放,使用pprof分析;3) 使用非阻塞I/O和连接池提升网络性能,结合HTTP/2和负载均衡技术;4) 通过性能分析、代码优化、缓存和压缩等手段进一步提升服务性能。掌握这些技术能帮助开发者构建更高效稳定的服务。
|
1月前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
78 1