A worker pool on a channel

Read the explanation
What if

Two workers range over a jobs channel. close(jobs) tells them no more jobs are coming.

012345678910main: start 2 workersgo ×2main: send jobs 1-4sendmain: close(jobs)closewg.Wait()main: all jobs donedoneworker 1: range: waitswaitsjob 1printjob 3worker 1: print, range endsendsworker 2: range: waitswaitsjob 2printjob 4worker 2: print, range endsends

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
  • Waiting on I/O
Step 1/6

main starts two workers. Each waits in range for a job: a dump shows [chan receive].

1func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {2    defer wg.Done()3    for j := range jobs { // ends once jobs is closed and empty4        time.Sleep(10 * time.Millisecond) // do job j: a network call, say5        fmt.Println("worker", id, "did job", j)6    }7}8 9func main() {10    jobs := make(chan int, 4)11    var wg sync.WaitGroup12    for id := 1; id <= 2; id++ {13        wg.Add(1)14        go worker(id, jobs, &wg)15    }16    for j := 1; j <= 4; j++ {17        jobs <- j18    }19    close(jobs) // no more jobs20    wg.Wait()21    fmt.Println("all jobs done")22}

Explanation

What just happened

main started two workers, sent four jobs into a buffered channel and closed it. Each worker ranged over jobs: take a job, do it, take the next. Which worker got which job was up to timing.

close(jobs) doesn’t throw jobs away. It means “no more are coming”. The workers finished the jobs already in the buffer, then their loops ended, they called Done, and wg.Wait() returned.

Try the what-if switch

Why the pool shape works

The channel is the queue and the lock in one. Each job is received by exactly one worker, so no job runs twice, and you change the pool size by changing one number.

Watch out

Go only reports a deadlock when every goroutine is stuck. Its dump shows where each one was waiting, like the lanes here.

Common questions

How does range over a channel end?

for j := range jobs ends when jobs is closed and every value in it has been received. If nobody closes it, the loop waits forever in [chan receive].

Who should close a channel?

The sender, once it has sent its last value. Sending on a closed channel panics with send on closed channel.

Which worker gets which job?

Whichever is free when the job is received. It can change on every run, so don't depend on it.

Verified: this timeline matches a real program, checked in CI on Go 1.26 and 1.27. How we verify. Unclear or wrong? Tell us