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

相关推荐
wn53141 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1231 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper2 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文3 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people3 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政8 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师10 小时前
spring获取当前request
java·后端·spring
Java小白笔记11 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___13 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server14 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php