结论
defer一个带参数的闭包,闭包的参数会立即求值
defer作用域外的函数,函数的参数也会立即求值
即参数值在执行到defer将延迟函数压入栈时已经初始化
背景:最近写项目时踩了坑,多个离线服务中需要埋点(服务启动时,调用函数向数据库插入一条记录,并且拿到ID,服务执行完毕后根据ID去更新记录的状态码和信息,标志着服务运行成功或者失败,显示失败原因)。
思路:利用Go中defered函数延迟执行的特性
实现方式:
版本1:每个服务中插入如下代码
/* ... */
id, err := StartEventTrack(ctx, servicePlatform, serviceName)
defer func() {
status, msg := 0, ""
if err != nil {
status = 1
msg = err.Error()
}
if _err := EndEventTrack(ctx, id, status, msg); _err != nil {
log.Println("[ERROR] EndEventTrack failed, err:", _err)
}
}
if err != nil {
log.Println("[ERROR] StartEventTrack failed, err:", err)
}
/* ... */
测试通过,但是每个模块都插入了重复代码,把对`status`,`msg`的处理封装为一个函数,这样每个模块只需要调用函数即可,大量减少重复
版本2:每个服务中插入如下代码
/* ... */
func EndEventTrackWrapper(ctx context.Context, id int, err error) {
status, msg := 0, ""
if err != nil {
status = 1
msg = err.Error()
}
if _err := EndEventTrack(ctx, id, status, msg); _err != nil {
log.Println("[ERROR] EndEventTrack failed, err:", _err)
}
}
id, err := StartEventTrack(ctx, servicePlatform, serviceName)
defer EndEventTrackWrapper(ctx, id, err) // 这里写法有错误,下文中纠正
if err != nil {
log.Println("[ERROR] StartEventTrack failed, err:", err)
}
/* ... */
在后续测试中,结束埋点时错误了,程序执行错误退出时,仍然会更新数据库中对应记录的状态为0(代表成功)。
原因:执行到第14行defer,将EndEventTrackWrapper压入到栈中时,已经将函数传参中的`ctx`,`id`,`err`初始化为当前环境中的值并保存下来。函数`EndEventTrackWrapper`中依靠`err`的值去赋值`status`,`msg`导致了bug产生
版本三:正确写法
下面代码中延迟函数会在执行时才捕获作用域内变量,符合预期。
/* ... */
func EndEventTrackWrapper(ctx context.Context, id int, err error) {
status, msg := 0, ""
if err != nil {
status = 1
msg = err.Error()
}
if _err := EndEventTrack(ctx, id, status, msg); _err != nil {
log.Println("[ERROR] EndEventTrack failed, err:", _err)
}
}
id, err := StartEventTrack(ctx, servicePlatform, serviceName)
defer func(){ EndEventTrackWrapper(ctx, id, err) }()
if err != nil {
log.Println("[ERROR] StartEventTrack failed, err:", err)
}
/* ... */
测试代码
package main
import "fmt"
func main() {
var val = 532
var err1, err2 error
// 普通闭包 变量值靠捕获
defer func() {
fmt.Println("defer 1 -> val:", val, "err1:", err1, "err2:", err2)
}()
// 函数签名中只声明类型,变量传参,还是捕获需要测试,无意义
defer func(int, error, error) {
fmt.Println("defer 2 -> val:", val, "err1:", err1, "err2:", err2)
}(val, err1, err2)
// 函数签名中命名变量名字和作用域中的相同
defer func(val int, err1, err2 error) {
fmt.Println("defer 3 -> val:", val, "err1:", err1, "err2:", err2)
}(val, err1, err2)
// 函数签名中命名变量名字和作用域中的不同
defer func(v int, e1, e2 error) {
fmt.Println("defer 4 -> v:", val, "e1:", e1, "e2:", e2)
}(val, err1, err2)
// defer 作用域外函数
defer fun1(val, err1, err2)
// err2赋值测试 defer 中函数参数初始化时机
err2 = fmt.Errorf("manual error2")
defer fun2(val, err1, err2)
err1 = fmt.Errorf("manual error1")
}
func fun1(val int, err1, err2 error) {
fmt.Println("defer 5 -> val:", val, "err1:", err1, "err2:", err2)
}
func fun2(val int, err1, err2 error) {
fmt.Println("defer 6 -> val:", val, "err1:", err1, "err2:", err2)
}
defer 6 -> val: 532 err1: <nil> err2: manual error2
defer 5 -> val: 532 err1: <nil> err2: <nil>
defer 4 -> v: 532 e1: <nil> e2: <nil>
defer 3 -> val: 532 err1: <nil> err2: <nil>
defer 2 -> val: 532 err1: manual error1 err2: manual error2
defer 1 -> val: 532 err1: manual error1 err2: manual error2