Go for循环内goroutine操作map无法捕获、追加值问题求解
问题根因
你遇到的两个问题全是并发时序错误,外加并发粒度设计不合理,具体拆解:
item打印为空的核心原因:wg.Wait()位置完全错了。你把等待所有goroutine完成的逻辑放在了所有Sheet、所有行的循环最外层,遍历完单行的单元格后,根本没等处理当前行单元格的goroutine执行写入,就直接打印item,此时goroutine大概率还没获得调度执行,map里自然没有值。- goroutine内打印
item[index]得到<nil>的原因:打印逻辑写在了赋值逻辑前面。你刚算出key就去读map,这时候还没执行item[index] = cell的写入操作,读到的必然是零值nil,和并发没关系,就是顺序写反了。 - 隐藏风险:你虽然给map写操作加了锁,但是外层循环无等待直接读map的操作没有加锁,属于并发读写map,Go原生map遇到并发读写会直接触发panic,你现在没触发只是因为读的时候goroutine还没开始写,属于运气好。
- 设计误区:给每个单元格开goroutine完全是负优化。单元格值解析是纳秒级的轻量CPU操作,goroutine创建、调度、锁竞争的开销远大于解析本身,万级数据量根本不需要这么细的并发粒度,真要提效按行维度控制并发数即可,无限制开goroutine反而会导致性能骤降。
修复方法
首先调整WaitGroup的等待位置:每遍历完一行的所有单元格,立刻等待当前行对应的所有goroutine执行完成,确认item填充完毕后再执行后续业务逻辑;其次如果要在goroutine内打印值,把打印逻辑移到赋值之后,读map记得加读锁。
以下是修正并发时序后的可运行代码:
var wg sync.WaitGroup var mapMutex sync.RWMutex for _, sheet := range originFile.Sheets { // 提前提取表头,避免每个goroutine重复访问 header := sheet.Rows[0].Cells for _, row := range sheet.Rows { item := map[string]interface{}{} for cellIdx, cell := range row.Cells { wg.Add(1) // 循环变量副本已正确处理 cell := cell cellIdx := cellIdx go func() { defer wg.Done() index := fmt.Sprintf("%v", header[cellIdx]) mapMutex.Lock() item[index] = cell mapMutex.Unlock() // 要打印值必须放到赋值后,且加读锁 // mapMutex.RLock() // logrus.Info(item[index]) // mapMutex.RUnlock() }() } // 等当前行所有单元格处理完成,item已填充完毕 wg.Wait() // 此处可正常获取完整item执行业务逻辑 logrus.Info(item) // 后续数据库插入等逻辑放在这里即可 } }
更合理的优化方案
直接去掉单元格级的goroutine,串行处理单行单元格,性能比加goroutine加锁更高,代码也更简单不易出错:
for _, sheet := range originFile.Sheets { header := sheet.Rows[0].Cells for _, row := range sheet.Rows { item := map[string]interface{}{} for cellIdx, cell := range row.Cells { index := fmt.Sprintf("%v", header[cellIdx]) item[index] = cell } // 直接拿到完整item处理 logrus.Info(item) // 若要提升导入速度,在此处按行维度开goroutine即可,建议配合固定大小的worker池控制并发数(比如同时开10个协程处理行),避免瞬间打满数据库连接 } }
内容的提问来源于stack exchange,提问作者Pocket
相关产品推荐
相关产品推荐

