用自定义io.Writer实现ProgressWriter,在Write中累加并回调进度;需避开io.MultiWriter陷阱,正确包装os.File;total应预估或设为-1;非终端环境降级输出;核心是total可靠性比计数更重要。

用自定义io.Writer在io.Copy中捕获写入进度
直接调io.Copy(dst, src)拿不到进度,因为标准库不暴露中间字节数。必须把dst换成你自己的io.Writer实现,在Write方法里累加并回调——这是最轻量、最可控的方式。
- 别在
Write里做fmt.Printf或log.Println:每 32KB 触发一次(默认缓冲),1GB 文件会打上万行日志,拖慢整个流程 - 回调函数建议走带缓冲的
chan struct{ read, total int64 },由单独 goroutine 节流消费(例如只在read % (total/100) == 0时上报) -
total不能硬写死:如果是生成文件(如加密导出、日志归档),应提前计算好目标大小;若无法预知(如压缩流、加密流),就传-1,进度条退化为速率显示模式 - 注意
os.O_CREATE | os.O_WRONLY | os.O_TRUNC打开文件,避免追加写导致内容错位,影响后续Stat().Size()校验
ProgressWriter必须避开io.MultiWriter陷阱
io.MultiWriter是广播器,不是代理器。把它和os.File一起塞进去,会导致Write被调两次:一次进文件,一次进你的计数器——但两个路径的写入节奏不同步,read值会跳变、重复甚至超总长。
- 正确做法是让
ProgressWriter直接包装os.File,io.Copy只写它一个目标 - 如果还要同时写日志或审计流,得在
ProgressWriter.Write内部手动调用其他Write,并确保原子性(比如用sync.Mutex保护共享状态) - 别依赖
os.File.Stat().Size()来校验实时进度:它反映的是上次Sync()或关闭后的大小,中间可能滞后几MB
生成过程本身不可控时,换用io.TeeReader反向注入
有些场景“生成”其实是读取+转换:比如从数据库查记录、序列化成 JSONL 写入文件,或解密原始流再落盘。这时数据源是io.Reader,你控制不了它的Read节奏,但可以插在它和io.Copy之间做分流。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 用
io.TeeReader(src, progressWriter),其中progressWriter只实现Write做计数,不真正存储数据 - 常见错误是传
&count这种裸指针:它不满足io.Writer接口,必须包装成结构体并实现Write([]byte) (int, error) - 如果生成逻辑含阻塞操作(如网络请求、SQL 查询),进度回调里千万别同步等它——否则
io.Copy会卡住。回调只负责发信号,处理交给异步 goroutine
终端非 TTY 环境下进度输出自动降级
命令行工具跑在 CI、重定向输出(./gen > out.log)或 SSH 非交互会话里时,\r不会回车覆盖,而是变成乱码字符堆满日志。
立即学习“go语言免费学习笔记(深入)”;
- 必须检测
os.Stdout.Fd()是否为终端:isatty.IsTerminal(os.Stdout.Fd())(用github.com/mattn/go-isatty) - 非终端环境改用简单日志:每秒打印一次
fmt.Printf("wrote %s / %s\n", humanize.Bytes(read), humanize.Bytes(total)) - 别自己手撸
\r逻辑:成熟进度库(如golang.org/x/exp/progress或github.com/schollz/progressbar/v3)都内置了这层判断
total的可靠性,比计数器本身更重要。**

















