Skip to content
v1See what changed

A progress bar that stays out of the hot path

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
main.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()
  • 74×faster to advance than the untagged version
  • 220×faster for a bar of 1,000 updates on a terminal
  • 0allocations to advance or redraw the bar
  • 44 nsto redraw the bar, written in one Write

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

Faster than the untagged version, with nothing to allocate

Section titled “Faster than the untagged version, with nothing to allocate”

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.

Time per Advance, lower is better
Show as a table
Time per Advance: time and allocations, untagged against v1
Scenariountaggedv1
1 goroutine133.0 ns1 alloc1.8 ns0 allocs
16 goroutines at once313.2 ns2 allocs59.6 ns0 allocs
Verbose, 1 goroutine23.56 µs13 allocs1.8 ns0 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.

untagged
bar := progress.New(0, n, progress.Options{})
_, _ = bar.Start()
for _, f := range files {
process(f)
_, _ = bar.Advance(1)
}
_, err := bar.Stop()
v1
bar := progress.New(n, progress.Options{})
bar.Start()
for _, f := range files {
process(f)
bar.Advance(1)
}
err := bar.Stop()

Draw your first bar in five minutes

Install the module, wrap a loop in Start, Advance and Stop, and watch it count. The guides cover bytes, goroutines and verbose output.