大宽表导入导出需控制并发与资源独占:导入时用固定数量worker消费channel,每个worker独占解析器并及时关闭文件;导出时须单协程流式写入,避免并发写导致竞态或性能下降。

大宽表导入时别让 goroutine 无节制拉起
宽表(比如 50+ 列)导入常伴随大量字段校验、类型转换和数据库写入,直接 for _, row := range rows + go processRow(row) 会瞬间拉起成百上千 goroutine,内存暴涨、调度压垮 runtime,且所有 goroutine 共享同一个 csv.Reader 或 excelize.File 实例,导致读取错位或 panic。
- 必须用固定数量 worker 消费 channel:worker 数建议设为
runtime.NumCPU()或 4–16,I/O 密集型任务可略高 - 每个 worker 独占解析器:对 CSV,每个 worker 自己 new
csv.NewReader(io.Reader);对 Excel,每个 worker 调excelize.OpenReader(bytes.NewReader(data)),不能复用同一*excelize.File - 上传文件流不能在 main defer 关闭:
multipart.File必须在 worker 内部defer file.Close(),否则多个 worker 争抢同一句柄
导出宽表时 csv.Writer 缓冲和并发写要分开
宽表导出不是“越快越好”,而是“越稳越省”。直接用 csv.NewWriter(f) 写 10 万行宽表,每行 50 字段,系统调用频次爆炸,性能反而比单协程差;若用多个 goroutine 并发写同一文件句柄,还会触发 data race 或内容错乱。
- 导出必须单协程流式写:用
bufio.NewWriterSize(f, 1 包一层,再传给 <code>csv.NewWriter(buf),最后依次调w.Flush()和buf.Flush() - 并发只用于数据准备:比如分页查数据库,每页 1000 行启动一个 goroutine 做字段映射和字符串转换,结果发到带缓冲 channel(如
make(chan [][]string, 100)),主 goroutine 顺序消费写入 - 宽表字段名别硬编码:首行写入前用
reflect.StructTag提取结构体的csv:"name"tag,生成 header slice,避免列顺序错位
Gin 中处理超长请求要主动限流和超时
宽表导入导出接口容易被误当“后台任务”反复重试,Gin 默认不设限,一个 200MB 的 Excel 上传可能卡住整个路由组,后续请求排队等待,连接数耗尽。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
gin-contrib/timeout中间件:对/import接口设timeout: 300s,避免慢解析拖垮服务 - 文件大小硬限制:在
gin.Engine.MaxMultipartMemory设为 128MB(默认 32MB),并配合前端上传控件做前置校验 - 禁止长轮询式重试:返回 JSON 里带
"job_id": "import_abc123",由前端轮询/job/status?id=abc123,而不是反复 POST 同一文件
Excel 宽表读写必须绕开 GetRows() 的空行陷阱
excelize.GetRows("Sheet1") 对宽表极不友好:它会把合并单元格、空行、隐藏列全算作“一行”,返回的 [][]string 长度不稳定,第 30 列可能是空字符串也可能是 nil,直接按索引取值大概率 panic 或错位。
立即学习“go语言免费学习笔记(深入)”;
- 改用
f.GetSheetRow(sheet, row):逐行拉取原始单元格值,空单元格返回"",不会跳行也不会崩 - 宽表列数动态识别:先用
f.GetSheetMap()或遍历f.GetSheetList()确认 sheet 名,再用f.GetSheetRow(sheet, 1)拿 header,建map[string]int映射列名到 index - 单元格类型必须显式判断:
cellType := f.GetCellType(sheet, "A1"),cellType == excelize.CellTypeNumeric时再用f.GetCellValue+ 手动转 time 或 int,不能直接fmt.Sprintf("%v")

















