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

相关推荐
盛派网络小助手2 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
zquwei3 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
dessler3 小时前
Docker-run命令详细讲解
linux·运维·后端·docker
Q_19284999064 小时前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
ZSYP-S4 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
Yuan_o_5 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
程序员一诺6 小时前
【Python使用】嘿马python高级进阶全体系教程第10篇:静态Web服务器-返回固定页面数据,1. 开发自己的静态Web服务器【附代码文档】
后端·python
DT辰白6 小时前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构