I/O-bound work is limited primarily by input/output speed or an external system, such as a REST call or database query. CPU-bound work instead waits on computation, while memory-bound work waits on available memory. section 8.5
A common concurrency mistake is sizing I/O-bound goroutines like CPU-bound goroutines. CPU-bound concurrency should stay near GOMAXPROCS; I/O-bound concurrency depends on factors such as the external system. section 8.5 section 8.7
A worker-pooling implementation makes the workload and its coordination visible:
- Read fixed-size chunks from an io.Reader and process each chunk sequentially. section 8.5
- Create a fixed pool of goroutines that receives chunks from a shared channel. section 8.5
- Protect the shared result with atomic updates, then close the channel and wait for every worker. section 8.5
- Treat the pool size as an external-system decision rather than automatically setting it to GOMAXPROCS. section 8.7
func read(r io.Reader) (int, error) {
count := 0
for {
b := make([]byte, 1024)
_, err := r.Read(b) #1
if err != nil {
if err == io.EOF { #2
break
}
return 0, err
}
count += task(b) #3
}
return count, nil
}
The sequential listing shows the baseline: reading 1,024-byte chunks and applying task before accumulating the result. section 8.5
func read(r io.Reader) (int, error) {
var count int64
wg := sync.WaitGroup{}
var n = 10
ch := make(chan []byte, n) #1
wg.Add(n) #2
for i := 0; i < n; i++ { #3
go func() {
defer wg.Done() #4
for b := range ch { #5
v := task(b)
atomic.AddInt64(&count, int64(v))
}
}()
}
for {
b := make([]byte, 1024)
// Read from r to b
ch <- b #6
}
close(ch)
wg.Wait() #7
return int(count), nil
}
The worker-pool listing contrasts the baseline with concurrent processing through a channel, WaitGroup, and atomic counter. section 8.5




