如何为Go并发下载进程添加单/多个进度条?
多进度条实现方案(基于
github.com/cheggaaa/pb/v3) 核心思路
解决动态content-length问题的关键是提前获取每个曲目的真实总下载大小,不管是普通单文件还是HLS分段文件,再为每个曲目创建对应长度的进度条,最后通过进度池统一管理所有进度条的显示。
步骤1:安装依赖
go get github.com/cheggaaa/pb/v3
步骤2:修改播放列表下载逻辑
if soundData.Kind == "playlist" { var wg sync.WaitGroup plDownloadTracks := getPlaylistDownloadTracks(soundData, clientId) // 初始化进度池,统一管理所有进度条 pool := pb.NewPool() pool.SetMaxWidth(80) // 设置进度条显示宽度 for _, dlT := range plDownloadTracks { wg.Add(1) go func(dlT []soundcloud.DownloadTrack) { defer wg.Done() t := getTrack(dlT, true) // 提前获取当前曲目的总下载大小 totalSize, err := getTrackTotalSize(t) if err != nil { fmt.Printf("获取曲目「%s」大小失败: %v\n", t.SoundData.Title, err) return } // 创建对应长度的进度条,设置显示名称 bar := pb.New64(totalSize).SetDescription(fmt.Sprintf("下载: %s", t.SoundData.Title)) pool.Add(bar) bar.Start() // 启动当前进度条 fp := soundcloud.Download(t, downloadPath, bar) if fp != "" { soundcloud.AddMetadata(t, fp) } bar.Finish() // 结束进度条(无论文件是否已存在) }(dlT) } pool.Start() // 启动进度池 wg.Wait() // 等待所有下载完成 pool.Stop() // 停止进度池 fmt.Printf("\n%s 播放列表已保存至: %s\n", theme.Green("[-]"), theme.Magenta(downloadPath)) return }
步骤3:添加获取曲目总大小的工具函数
// getTrackTotalSize 获取曲目真实总下载大小,兼容普通文件和HLS分段 func getTrackTotalSize(track soundcloud.DownloadTrack) (int64, error) { if track.Quality != "low" { // HLS格式:先解析m3u8拿到所有分段URL,再逐个请求获取分段大小 resp, err := http.Get(track.Url) if err != nil { return 0, err } defer resp.Body.Close() segments := getSegments(resp.Body) var total int64 = 0 for _, segURI := range segments { segResp, err := http.Head(segURI) if err != nil { continue // 忽略单个分段的错误,继续统计其他分段 } total += segResp.ContentLength segResp.Body.Close() } return total, nil } else { // 普通格式:直接请求头获取文件大小 resp, err := http.Head(track.Url) if err != nil { return 0, err } defer resp.Body.Close() return resp.ContentLength, nil } }
步骤4:修改下载相关函数,适配进度条
更新Download函数
func Download(track soundcloud.DownloadTrack, dlpath string, bar *pb.ProgressBar) string { trackName := track.SoundData.Title + "[" + track.Quality + "]." + track.Ext path := validateDownload(dlpath, trackName) if path == "" { return "" // 文件已存在,直接返回 } if track.Quality != "low" { resp, err := http.Get(track.Url) if err != nil { return "" } defer resp.Body.Close() segments := getSegments(resp.Body) if err := DownloadM3u8(path, bar, segments); err != nil { return "" } return path } resp, err := http.Get(track.Url) if err != nil { return "" } defer resp.Body.Close() f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return "" } defer f.Close() // 同时写入文件和进度条,自动更新进度 _, err = io.Copy(io.MultiWriter(f, bar), resp.Body) return path }
更新DownloadM3u8和downloadSeg函数
func downloadSeg(wg *sync.WaitGroup, segmentURI string, file *os.File, bar *pb.ProgressBar) { defer wg.Done() resp, err := http.Get(segmentURI) if err != nil { return } defer resp.Body.Close() // 写入文件的同时更新进度条 if bar != nil { _, _ = io.Copy(io.MultiWriter(file, bar), resp.Body) } else { _, _ = io.Copy(file, resp.Body) } } func DownloadM3u8(filepath string, bar *pb.ProgressBar, segments []string) error { file, err := os.OpenFile(filepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { return err } defer file.Close() var wg sync.WaitGroup for _, segment := range segments { wg.Add(1) go downloadSeg(&wg, segment, file, bar) } wg.Wait() return nil }
关键说明
- 真实进度计算:通过
getTrackTotalSize提前获取每个曲目的总下载大小,避免使用常量值导致进度条失真。 - 进度池管理:
pb.Pool会自动处理多个进度条的布局和刷新,无需手动控制输出位置。 - HLS分段适配:每个分段的下载数据都会同步写入进度条,自动累加完成进度。
内容的提问来源于stack exchange,提问作者AYehia0
相关产品推荐
相关产品推荐

