One atomic add
Advance adds to a counter and returns. It never draws and never allocates, so it fits in the tightest
loop.
Count work from any number of goroutines with one atomic add. On a terminal the bar redraws itself in place ten times a second; anywhere else it writes one plain line when you stop it.
go get github.com/vardius/progress-go // Advance after each step. Stop draws the// bar one last time and ends its line.n := int64(len(files))bar := progress.New(n, progress.Options{})bar.Start()for _, f := range files { process(f) bar.Advance(1)}err := bar.Stop()// The total is the file's size, and the// counter advances the bar by each write.bar := progress.New(info.Size(), progress.Options{})bar.Start()w := io.MultiWriter(dst, counter{bar})_, err := io.Copy(w, src)err = errors.Join(err, bar.Stop())// Workers share one bar: Advance is an// atomic add, safe from any goroutine.bar := progress.New(n, progress.Options{})bar.Start()var wg sync.WaitGroupfor range 4 { wg.Go(func() { for job := range jobs { process(job) bar.Advance(1) } })}wg.Wait()err := bar.Stop()Features
One atomic add
Advance adds to a counter and returns. It never draws and never allocates, so it fits in the tightest
loop.
Workers share a bar
Call Advance from as many goroutines as you like. Start and Stop are safe from any of them too.
Calm on a terminal
A timer redraws the bar in place ten times a second, however fast the work goes, so the terminal never floods and a slow job’s times keep ticking.
Plain in logs
Written to a file, a pipe or a buffer, the bar is one line at Stop, with no escape codes.
Exact at the edges
Integer maths for any int64 count. The percentage stays within 0 and 100%, and a job stopped part way
shows where it stopped.
Verbose without pauses
The heap size comes from runtime/metrics, which doesn’t stop the world, beside the time elapsed, the
estimated total and the time remaining.
Performance
Advance no longer draws: it is one atomic add. A timer redraws the bar ten times a second, formatting it
into a reused buffer and writing it in one call, and the verbose heap size no longer stops the world.
With 16 goroutines at once, the time is wall time per call across all of them, so it measures throughput.
| Scenario | untagged | v1 |
|---|---|---|
| 1 goroutine | 133.0 ns1 alloc | 1.8 ns0 allocs |
| 16 goroutines at once | 313.2 ns2 allocs | 59.6 ns0 allocs |
| Verbose, 1 goroutine | 23.56 µs13 allocs | 1.8 ns0 allocs |
Apple M4 Max, Go 1.27.0, median of 5 runs.
| Scenario | untagged | v1 |
|---|---|---|
| 1,000 updates | 936.72 µs2781 allocs | 4.26 µs6 allocs |
| 1,000 updates, verbose | 25.02 ms10799 allocs | 4.83 µs8 allocs |
Apple M4 Max, Go 1.27.0, median of 5 runs.
Upgrading
The options and the bar on screen are as before. The calls return less, a total replaces start and max, and
Stop now shows where a job stopped instead of filling the bar.
bar := progress.New(0, n, progress.Options{})_, _ = bar.Start()for _, f := range files { process(f) _, _ = bar.Advance(1)}_, err := bar.Stop()bar := progress.New(n, progress.Options{})bar.Start()for _, f := range files { process(f) bar.Advance(1)}err := bar.Stop()Install the module, wrap a loop in Start, Advance and Stop, and watch it count. The guides cover bytes, goroutines and verbose output.