学习目标:
- 了解进度条运行原理
- 掌握github.com/cheggaaa/pb第三方依赖的函数
- 实践一个进度条
学习内容:
前置说明:
本篇文章所讲解的进度条是由github.com/cheggaaa/pb依赖包提供的。
https://github.com/cheggaaa/pb作者官方文档,请点击
为什么要用https://github.com/cheggaaa/pb依赖包?
- https://github.com/cheggaaa/pb是基于golang语言开发的,可以与golang语言开发的项目直接兼容。
- https://github.com/cheggaaa/pb中有大量的函数,可以丰富进度条
一个简单的进度条案列
package main
import (
"time"
"github.com/cheggaaa/pb"
)
func main() {
//总量 一般是不变的定值
count := 1000
//创建一个总量是5000的进度条
bar := pb.New(count)
// 显示百分比
bar.ShowPercent = true
// 显示进度条
bar.ShowBar = true
//显示 [1/5000]
bar.ShowCounters = true
//在进度条右侧显示时间 500ms 【Left是左的意思,为这么显示在右侧。我也不知道,不去纠结】
bar.ShowTimeLeft = true
// 进度条启动
bar.Start()
for i := 0; i < count; i++ {
//pb.Add(1) 每次执行一次这个函数,进度条的当前值 +1
bar.Increment()
//睡眠1秒
time.Sleep(time.Millisecond)
}
//结束进度条 pb.Finish() 并打印信息
bar.FinishPrint("The End!")
}
效果:
这种进度条可以运用在做包子的情景上。假设一共要做1000个包子。每做一个包子,就执行一次bar.Increment()操作。这里就可以引入多线程,每个线程触发一次bar.Increment(),进度条就+1.
package main
import (
"github.com/cheggaaa/pb"
"sync"
"time"
)
func main() {
//总量 一般是不变的定值
count := 1000
//创建一个总量是5000的进度条
bar := pb.New(count)
// 显示百分比
bar.ShowPercent = true
// 显示进度条
bar.ShowBar = true
//显示 [1/5000]
bar.ShowCounters = true
//在进度条右侧显示时间 500ms 【Left是左的意思,为这么显示在右侧。我页不知道,不去纠结】
bar.ShowTimeLeft = true
// 进度条启动
bar.Start()
//等待组
var wg sync.WaitGroup
//等待总量
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
//等待总量 -1
defer wg.Done()
//pb.Add(1) 每次执行一次这个函数,进度条的当前值 +1
bar.Increment()
//睡眠1秒
time.Sleep(time.Millisecond)
}()
}
//只有等待总量为0,才会放行
wg.Wait()
//结束进度条 pb.Finish() 并打印信息
bar.FinishPrint("The End!")
}
效果:
在这里,我使用到了sync.WaitGroup等待组技术。sync.WaitGroup与进度条运行规律是相反的。我们看一下他们的改变量的函数源码。
sync.WaitGroup:
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
进度条:
// Increment current value
func (pb *ProgressBar) Increment() int {
return pb.Add(1)
}
总体来说,满足进度条运行必须有如下条件:必须有总量值和进度推动操作。
多个进度条的联合使用
package main
import (
"github.com/cheggaaa/pb"
"math/rand"
"sync"
"time"
)
func main() {
// 创建多个进度条 设置前缀 总量为200
first := pb.New(200).Prefix("First ")
second := pb.New(200).Prefix("Second ")
third := pb.New(200).Prefix("Third ")
// pool类似于进度条的池子 并启动进度条
pool, err := pb.StartPool(first, second, third)
if err != nil {
panic(err)
}
// 推动进度
wg := new(sync.WaitGroup)
//[]*pb.ProgressBar{first, second, third} 定义了一个进度条为元素的切片
for _, bar := range []*pb.ProgressBar{first, second, third} {
wg.Add(1)
go func(cb *pb.ProgressBar) {
for n := 0; n < 200; n++ {
cb.Increment()
//随机时间
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
}
cb.Finish()
wg.Done()
}(bar)
}
wg.Wait()
// 关闭
pool.Stop()
}
效果:
多个进度条的联合使用,一般与多线程搭配的。之前的案列是多个线程共同完成1000个包子,而这个案例是多个线程各自完成各自的任务数量。
课外技术点讲解:
- rand.Intn(100) 随机数生成,0<随机数<100
- time.Millisecond * time.Duration(rand.Intn(100)) 中的Duration()表示相乘。
说明:因为time.Millisecond 只能与int类型的常量相乘。而rand.Intn(100)是随机数。
进度条在文件Copy、IO流的运行
本机环境测试:
package main
import (
"fmt"
"github.com/cheggaaa/pb"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
//指定文件的起始路径和目标路径 将E:/static/source中的文件 克隆入 E:/static/dest 中
sourceName, destName := "E:/static/source/hang.txt", "E:/static/dest/hang.txt"
// check source
var source io.Reader
var sourceSize int64
if strings.HasPrefix(sourceName, "http://") {
// 当sourceName 是网站地址时
resp, err := http.Get(sourceName)
if err != nil {
fmt.Printf("Can't get %s: %v\n", sourceName, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Server return non-200 status: %v\n", resp.Status)
return
}
i, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
sourceSize = int64(i)
source = resp.Body
} else {
// sourceName 是本机上的一个目录 打开
s, err := os.Open(sourceName)
if err != nil {
fmt.Printf("Can't open %s: %v\n", sourceName, err)
return
}
defer s.Close()
// 获取sourceName的目录信息 并 检测是否存在
sourceStat, err := s.Stat()
if err != nil {
fmt.Printf("Can't stat %s: %v\n", sourceName, err)
return
}
//获取存储大小
sourceSize = sourceStat.Size()
source = s
}
// 创建转移后的路径 我这里是创建好了的,没有创建的,会自动创建
dest, err := os.Create(destName)
if err != nil {
fmt.Printf("Can't create %s: %v\n", destName, err)
return
}
defer dest.Close()
// 创建进度条 总量为源文件的size | SetUnits(pb.U_BYTES) 设置文件大小的单位 KB
bar := pb.New(int(sourceSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10)
bar.ShowSpeed = true
bar.Start()
// 创建一个读对象
reader := bar.NewProxyReader(source)
// 克隆
io.Copy(dest, reader)
//关闭bar
bar.Finish()
}
效果:
这是一个文件的Copy操作。自定义原路径和目标路径。可以看出,进度条的总量是源文件的size大小。这里要说一下,我这个文件size很小,所以时间几乎为0.
线上环境测试:
package main
import (
"fmt"
"github.com/cheggaaa/pb"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
func main() {
//指定文件的起始路径和目标路径 将E:/static/source中的文件 克隆入 E:/static/dest 中
sourceName, destDir := "https://img01.png", "E:/static/dest/"
// check source
var source io.Reader
var sourceSize int64
if strings.HasPrefix(sourceName, "http://") || strings.HasPrefix(sourceName, "https://") {
// 当sourceName 是网站地址时
resp, err := http.Get(sourceName)
if err != nil {
fmt.Printf("Can't get %s: %v\n", sourceName, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Server return non-200 status: %v\n", resp.Status)
return
}
i, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
sourceSize = int64(i)
source = resp.Body
} else {
// sourceName 是本机上的一个目录 打开
s, err := os.Open(sourceName)
if err != nil {
fmt.Printf("Can't open %s: %v\n", sourceName, err)
return
}
defer s.Close()
// 获取sourceName的目录信息 并 检测是否存在
sourceStat, err := s.Stat()
if err != nil {
fmt.Printf("Can't stat %s: %v\n", sourceName, err)
return
}
//获取存储大小
sourceSize = sourceStat.Size()
source = s
}
// 创建转移后的路径 我这里是创建好了的,没有创建的,会自动创建
destName := destDir + filepath.Base(sourceName)
dest, err := os.Create(destName)
if err != nil {
fmt.Printf("Can't create %s: %v\n", destName, err)
return
}
defer dest.Close()
// 创建进度条 总量为源文件的size | SetUnits(pb.U_BYTES) 设置文件大小的单位 KB
bar := pb.New(int(sourceSize)).SetUnits(pb.U_BYTES).SetRefreshRate(time.Millisecond * 10)
bar.ShowSpeed = true
bar.Start()
// 创建一个读对象
reader := bar.NewProxyReader(source)
// 克隆
io.Copy(dest, reader)
//关闭bar
bar.Finish()
}
学习总结:
- 实践成功进度条的使用
- 后续深入了解github.com/cheggaaa/pb依赖包的函数