用 io.MultiWriter 可将数据同时写入目标和进度计数器,避免修改数据源;自定义 Write 方法原子累加字节数,配合 Flush、context 控制超时与取消、分批事务、流式解析及节流回调,实现高效可控的带进度导入。

用 io.MultiWriter 把导入过程“镜像”到进度计数器
Go 里没有内置的“带进度的 Reader”,但你可以把原始数据流和进度统计逻辑耦合在同一个写入路径上。核心思路是:不改数据源,只在写入目标前加一层“透明代理”。io.MultiWriter 就是干这个的——它把一份数据同时发给多个 io.Writer,比如一个存文件,一个算字节数。
常见错误是试图在 Read() 过程中手动累加,结果发现 CSV 解析器、JSON 解码器这些库根本不走你重写的 Read(),它们内部用 bufio.Reader 或直接 syscall,你的钩子根本挂不上。
- 把原始数据(比如
*os.File或net/http.Response.Body)先包装成io.ReadCloser - 创建一个自定义的
io.Writer类型,只实现Write(p []byte) (n int, err error),里面做原子计数:atomic.AddUint64(&progress.bytes, uint64(len(p))) - 用
io.MultiWriter把这个计数器和真实目标(如*sql.Tx的批量插入器、或csv.NewWriter)串起来 - 注意:如果目标 Writer 内部有 buffer(比如
bufio.Writer),要记得在关键节点调用Flush(),否则计数会滞后
用 context.Context 控制超时与取消,别等“卡住”的导入自己醒
数据导入常卡在慢 SQL、网络抖动、大文件解压上。靠轮询 bytes 计数器判断“卡死”不可靠——可能只是当前 chunk 恰好小。真正该响应的是外部信号:context.WithTimeout 或 context.WithCancel。
容易踩的坑是把 context 只传给最外层函数,但底层数据库驱动(如 pgx)或解析库(如 gocsv)没接收到。结果 cancel 了 context,导入还在跑。
立即学习“go语言免费学习笔记(深入)”;
- 所有涉及 I/O 的调用链,从
http.Get到db.Exec,都必须显式传入ctx - 批量插入时,别用单个大事务包全量;按每 1000 行拆成子事务,并在每个子事务开始前检查
ctx.Err() != nil - 如果用
encoding/csv,它的Read()不接受context,得自己套一层带超时的io.LimitReader或用time.AfterFunc配合 channel select
runtime.GC 和内存暴涨无关,但 bufio.Scanner 默认 64KB 缓冲会吃掉大文件导入的性能
导入大 CSV 或 JSONL 文件时,程序 RSS 内存飙升,第一反应常是“GC 没触发”,其实 Go 的 GC 会自动工作。真正的问题常出在缓冲策略上:默认的 bufio.Scanner 用 64KB 缓冲读行,遇到超长行(比如某字段含 base64 图片)会自动扩容,一次分配几 MB,反复几次就 OOM。
另一个坑是误用 strings.Split 处理整文件内容,把几百 MB 的 []byte 全载入内存再切分,而不是流式处理。
- 用
bufio.NewReader替代Scanner,自己控制每次ReadSlice('\n')的大小上限 - 对 CSV,优先用
csv.NewReader并设置reader.FieldsPerRecord = -1和reader.TrailingComma = true,避免因格式问题 panic 后全量重试 - 导入前用
os.Stat().Size预估总量,若 >100MB,强制启用流式解析 + 分批 commit,别试图一次性 load all
进度回调不是越密越好,atomic.StoreUint64 在高并发导入下也需节流
有人每读一行就调用一次回调函数更新 UI 或写日志,结果导入速度掉一半——不是因为计算慢,而是频繁的原子操作+跨 goroutine 通信(比如 channel send)成了瓶颈。更糟的是,回调里做 HTTP 请求或 DB 查询,直接拖垮整个流程。
真正的“可感知进度”不需要每 KB 更新一次。用户关心的是“还有多久”,不是“此刻精确到字节”。
- 用滑动窗口法:只在每累计 1MB 或每 10 秒,才调用一次回调,用
atomic.LoadUint64读取当前值 - 回调函数内禁止阻塞操作;如需上报,用无缓冲 channel 异步丢给单独 goroutine 处理
- 如果导入本身是多 goroutine 并行(如分片读文件),别让每个 goroutine 都去更新同一个
atomic.Value;改用每个 goroutine 维护局部计数器,主 goroutine 定期汇总
进度跟踪最难的不是怎么记数,是怎么让记数这件事本身不影响主流程。越想“精确”,越容易把性能拖进泥里。留点余量,比实时抖动强。


















