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调用内建函数的 性能 比 直接调用内建函数 更差 。