用 fmt.Print 配合 \r 实现同一行覆盖输出,避免换行;推荐使用 mpb/v8 库处理并发、样式和终端适配问题,注意 Content-Length 获取、io.TeeReader 绑定及终端检测降级。

用 fmt.Print 覆盖同一行输出,而不是换行
命令行进度条本质是「不断更新同一行文本」,不是打印一堆新行。Golang 默认的 fmt.Println 会加 \n,导致每条都往下跑,根本叠不成条。必须用 fmt.Print 或 fmt.Printf,手动控制回车和覆盖逻辑。
关键点在于:每次输出末尾加 \r(回车符,回到行首),不加 \n(换行符),再输出新内容——后写的内容会覆盖前一次的显示。
- Windows 和 Linux/macOS 都支持
\r,无需额外兼容 - 别用
fmt.Println,哪怕只差一个ln,效果就完全错乱 - 如果终端不刷新(比如某些 IDE 内置终端),可加
os.Stdout.Sync()强制刷出
github.com/vbauerster/mpb/v8 是最稳的第三方进度条库
自己手撸 \r + 字符串拼接容易翻车:百分比对不齐、多线程下乱序、宽度适配失败、卡顿感明显。直接上成熟库更省心,mpb/v8 支持并发任务、嵌套进度、速率估算、自定义样式,且无 CGO 依赖,纯 Go 实现。
安装:go get github.com/vbauerster/mpb/v8
立即学习“go语言免费学习笔记(深入)”;
- 初始化要传
mpb.WithWidth(60)显式设宽度,否则默认按 80 列算,小终端里会折行 - 单任务场景用
p.AddBar即可;多文件下载等需并发时,用p.AddTracker+bar.SetTotal分别控制 - 别漏掉
bar.Increment()或bar.SetCurrent(),否则进度不动——常见错误是只调AddBar就以为完事了
下载文件时如何把 http.Response.Body 流式喂给进度条
进度条得知道「当前读了多少 / 总共多少」,而 http.Get 返回的 Body 是个 io.ReadCloser,本身不带总长度。必须先发 HEAD 请求或解析 Content-Length 响应头,再用 io.TeeReader 把字节流和计数器绑在一起。
- HEAD 请求可能被禁用,稳妥做法是先
resp, _ := http.Head(url),检查resp.Header.Get("Content-Length")是否非空 - 拿到总长度后,用
bar := p.AddBar(total, mpb.PrependDecorators(...))初始化 - 核心是:
reader := io.TeeReader(resp.Body, bar),之后所有从reader读出的字节都会自动触发bar.Increment() - 别直接
io.Copy(dst, resp.Body)—— 这样进度条完全收不到信号
终端宽度变化或重定向时进度条会崩,得提前防御
用户缩放终端、用 ./cmd > log.txt 重定向输出、甚至 SSH 断连重连,都会让 \r 行为异常:要么残留旧文字,要么整行消失。这不是你代码有 bug,而是终端状态不可靠。
- 检测是否在真实终端:
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))(需golang.org/x/term),不是就退化为纯日志输出,不画条 - 重定向场景下,
mpb会自动降级为「每秒打一行当前进度」,但得确保你没强行关掉它:mpb.WithOutput(os.Stdout)别硬塞非终端对象 - Ctrl+C 中断时,最后一行可能卡在中间状态,加
defer bar.Abort()或signal.Notify捕获中断,手动补个换行收尾


















