go语言中的延迟执行函数

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
性能测试 PTS,5000VUM额度
简介: 【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调用内建函数的 性能 比 直接调用内建函数 更差  。
目录
相关文章
|
7天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
24 2
|
5天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
13 2
|
5天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
16 2
|
8天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
5天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
13 4
|
5天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
18 1
|
7天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
18天前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
32 3
|
19天前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
26 3
|
3月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
131 1
go语言后端开发学习(四) —— 在go项目中使用Zap日志库