Go defer后函数签名中参数初始化时机

  • 结论

  1. defer一个带参数的闭包,闭包的参数会立即求值

  1. 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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值