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

相关推荐
来自星星的坤1 小时前
SpringBoot 与 Vue3 实现前后端互联全解析
后端·ajax·前端框架·vue·springboot
AUGENSTERN_dc1 小时前
RaabitMQ 快速入门
java·后端·rabbitmq
烛阴2 小时前
零基础必看!Express 项目 .env 配置,开发、测试、生产环境轻松搞定!
javascript·后端·express
燃星cro2 小时前
参照Spring Boot后端框架实现序列化工具类
java·spring boot·后端
追逐时光者4 小时前
C#/.NET/.NET Core拾遗补漏合集(25年4月更新)
后端·.net
FG.4 小时前
GO语言入门
开发语言·后端·golang
转转技术团队5 小时前
加Log就卡?不加Log就瞎?”——这个插件治好了我的精神
java·后端
谦行6 小时前
前端视角 Java Web 入门手册 5.5:真实世界 Web 开发——控制反转与 @Autowired
java·后端
uhakadotcom6 小时前
PyTorch 2.0:最全入门指南,轻松理解新特性和实用案例
后端·面试·github
bnnnnnnnn6 小时前
前端实现多服务器文件 自动同步宝塔定时任务 + 同步工具 + 企业微信告警(实战详解)
前端·javascript·后端