标准库:使用延迟函数:内建函数和自定义函数的调用

191 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情

1 延迟执行说明

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 注册动作紧邻资源申请成功的动作。

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

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

1.1 常见用法

  • 拦截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旧值。

1.2 使用性能,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

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

小结

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

在当前版本1.20,当需要执行次数更多,defer 的延迟计算反而让整体性能更好,不得不佩服go的调度算法高效。

多数场景,我们的程序对性能并不敏感,但是即使敏感的场景,deferred函数也可以很好的完成执行。 它的注册和调度执行已经得到较大优化。

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

        * 内建函数,大部分场景(除deletedefer调用内建函数的 性能 比 直接调用内建函数 更差  。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情