工作中踩坑记录(1) —— go func() 竞争条件

背景

​ 最近组会上组长检查线上项目功能的时候发现我负责的模块的一个列表的数据有问题,有两个字段逻辑对不上,然后我来检查代码找 bug。

​ 这是一个列表展示的业务,并发调用 3 个 rpc 接口获取数据,再组合返回给前端。

逻辑复现

下面这个 demo 复现原始错误的逻辑(线上环境是Go 1.19)

go 复制代码
type Foo struct {
    ID  string `json:"id"`
    Bar string `json:"bar"`
}

func main() {

    ids := []string{"5", "6", "7", "8", "9"}
    bars := []string{"a", "b", "c", "d", "e"}

    result := make([]Foo, len(ids))

    wg := sync.WaitGroup{}

    for i, id := range ids {
        wg.Add(2)
        go func(i int, id string) {
            defer wg.Done()
            fmt.Printf("任务1的索引号:%d\n", i)
            // 业务逻辑 ......
            result[i].ID = id
            return
        }(i, id)

        go func(int) { // 错误的闭包
            defer wg.Done()
            fmt.Printf("任务2的索引号:%d\n", i)
            // 业务逻辑 ......
            result[i].Bar = bars[i]
            return
        }(i)
    }
    wg.Wait()
    fmt.Printf("%+v", result)
}

输出结果:

bash 复制代码
任务1的索引号:0
任务1的索引号:2                                           
任务2的索引号:4                                           
任务1的索引号:1                                           
任务1的索引号:3                                           
任务1的索引号:4                                           
任务2的索引号:4                                           
任务2的索引号:4                                           
任务2的索引号:4                                           
任务2的索引号:4                                           
[{ID:5 Bar:} {ID:6 Bar:} {ID:7 Bar:} {ID:8 Bar:} {ID:9 Bar:e}]

​ 在第二个匿名函数中,由于错误的闭包导致它访问了外部的循环变量i。在内层goroutine开始执行之前,主协程的 for 循环已经在执行了,导致i的值发生了变化。

​ 这就导致了只为一个元素重复赋值。

修改方案

上面 demo 的 main 函数中的代码替换为一下内容

go 复制代码
ids := []string{"5", "6", "7", "8", "9"}
bars := []string{"a", "b", "c", "d", "e"}

result := make([]Foo, len(ids))

wg := sync.WaitGroup{}

for it, uid := range ids {
    wg.Add(2)
    go func(it int, uid string) {
        defer wg.Done()
        fmt.Printf("任务1的索引号:%d\n", it)
        // 业务逻辑 ......
        result[it].ID = uid
        return
    }(i, id)

    go func(it int) { // 修改后
        defer wg.Done()
        fmt.Printf("任务2的索引号:%d\n", it)
        // 业务逻辑 ......
        result[it].Bar = bars[it]
        return
    }(i)
}
wg.Wait()
fmt.Printf("%+v", result)

​ 在每个 goroutine 中创建一个局部变量it,并将当前迭代的i的值赋给这个局部变量。这样,每个 goroutine 都会持有自己的i的副本,避免了竞争条件。

输出结果:

bash 复制代码
任务1的索引号:2
任务2的索引号:1                                           
任务2的索引号:2                                 
任务2的索引号:0                                           
任务1的索引号:3                            
任务1的索引号:0                                           
任务2的索引号:3                                       
任务1的索引号:4                    
任务2的索引号:4                                        
任务1的索引号:1             
[{ID:5 Bar:a} {ID:6 Bar:b} {ID:7 Bar:c} {ID:8 Bar:d} {ID:9 Bar:e}]

IDE 提示修复

​ 其实在 Goland 里,第二个匿名函数内部赋值时变量i背景颜色被标黄了,鼠标悬停上去会显示 由 'go' 语句中的 'func' 文字捕获的循环变量可能有意外值

点击创建中间变量后,IDE 给出的修复方案如下

go 复制代码
type Foo struct {
    ID  string `json:"id"`
    Bar string `json:"bar"`
}

func main() {
    ids := []string{"5", "6", "7", "8", "9"}
    bars := []string{"a", "b", "c", "d", "e"}

    result := make([]Foo, len(ids))

    wg := sync.WaitGroup{}

    for i, id := range ids {
        wg.Add(2)
        go func(i int, id string) {
            defer wg.Done()
            fmt.Printf("任务1的索引号:%d\n", i)
            // 业务逻辑 ......
            result[i].ID = id
            return
        }(i, id)

        i := i // IDE 生成的修正代码
        go func(int) {
            defer wg.Done()
            fmt.Printf("任务2的索引号:%d\n", i)
            // 业务逻辑 ......
            result[i].Bar = bars[i]
            return
        }(i)
    }
    wg.Wait()
    fmt.Printf("%+v", result)
}

​ 这么做也是可行的,在go func(int) 函数之前添加了一行代码i := i。这实际上是创建了一个新的局部变量i,并将当前循环迭代中的i的值复制给它。这样,每个goroutine都有自己的i副本,可以安全地使用它来访问bars切片并将值赋给result切片的相应项的Bar字段。输出结果也符合预期。

总结

​ 在 Go 语言并发编程时,要注意竞争条件。注意go func()匿名函数的传参。

相关推荐
易安说AI6 小时前
Claude Opus 4.6 凌晨发布,我体验了一整晚,说说真实感受。
后端
易安说AI6 小时前
Ralph Loop 让Claude无止尽干活的牛马...
前端·后端
易安说AI6 小时前
用 Claude Code 远程分析生产日志,追踪 Claude Max 账户被封原因
后端
颜酱7 小时前
图结构完全解析:从基础概念到遍历实现
javascript·后端·算法
Coder_Boy_10 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
掘金者阿豪11 小时前
关系数据库迁移的“暗礁”:金仓数据库如何规避数据完整性与一致性风险
后端
ServBay11 小时前
一个下午,一台电脑,终结你 90% 的 Symfony 重复劳动
后端·php·symfony
sino爱学习11 小时前
高性能线程池实践:Dubbo EagerThreadPool 设计与应用
java·后端
颜酱11 小时前
从二叉树到衍生结构:5种高频树结构原理+解析
javascript·后端·算法
掘金者阿豪11 小时前
UUID的隐形成本:一个让数据库“慢下来”的陷阱
后端