工作中踩坑记录(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()匿名函数的传参。

相关推荐
GreenTea12 分钟前
AI 时代,工程师的不可替代性在哪里
前端·人工智能·后端
朦胧之32 分钟前
AI 编程开发思维
前端·后端·ai编程
希望永不加班2 小时前
Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别
java·开发语言·后端·spring·代理模式
浮游本尊3 小时前
一次合同同步背后的多阶段流水线:从外部主数据到本地歧义消解
后端
lv__pf3 小时前
springboot原理
java·spring boot·后端
段小二4 小时前
服务一重启全丢了——Spring AI Alibaba Agent 三层持久化完整方案
java·后端
UIUV4 小时前
Go语言入门到精通学习笔记
后端·go·编程语言
lizhongxuan4 小时前
开发 Agent 的坑
后端
段小二4 小时前
Agent 自动把机票改错了,推理完全正确——这才是真正的风险
java·后端