Skip to content
Lesson 3 of 9

Step 1 of 5 · Reading · ~4 min

Learn

Goroutines and Channels

Goroutines

By the end of this lesson you can start work that runs alongside the rest of your program, wait for it, and collect every result in order. Until now every statement ran one after another; this is the first lesson where two things run at once.

The go statement

Put go in front of a function call and the call runs as a goroutine: a function executing concurrently with its caller. The Go runtime decides when it gets a CPU. The caller does not wait.

go

Usually only from main appears. When main returns the whole program ends, and every goroutine still running is killed silently.

Function literals and closures

go is nearly always followed by a function literal, a function written inline with no name. It is a closure because it captures the variables around it, not copies of their values:

go

Both calls changed the same count, which causes the trap below.

Waiting with a WaitGroup

A sync.WaitGroup holds a count of unfinished goroutines. wg.Add(1) raises the count before you start a goroutine, wg.Done() lowers it as that goroutine's last act, and wg.Wait() blocks until the count is zero. Goroutines finish in any order, so give each one its own slot in a slice sized beforehand. This adds up every row of a table, one goroutine per row:

go

The (r, row) after the closing brace is the call: the literal is defined and invoked at once, and r and row are copied into k and vals when go runs. Each goroutine writes only sums[k], and sums is read only after Wait; print earlier and you see [0 0 0].

For a named function, pass the group as a pointer (wg *sync.WaitGroup, called with &wg, the & from the Pointers lesson). A plain sync.WaitGroup parameter is a copy, and Done on the copy never reaches the original. The arguments of a go call are evaluated immediately in the launching goroutine; only the body runs later.

The loop-variable trap

Before Go 1.22 a for loop had ONE variable for all iterations, so closures all share it:

go

The loop leaves i at 3 and every closure reads that one variable. Goroutines usually start after the loop ends, so they see its final value. In the row-sum program, reading r and row instead of k and vals makes every goroutine work on the last row, leaving sums[0] and sums[1] at 0. Two fixes work on every version: pass the value as an argument, or copy it first with r := r before go. This grader runs a pre-1.22 Go, so the bug is real here, though newer Go gives each iteration its own variable.

Data races

Two goroutines updating the same variable cause a data race. hits++ is read, add 1, write back; if two goroutines both read 5 before either writes, both write 6 and one increment is lost. Eight goroutines each doing hits++ 100000 times ended on this grader at 272,000 to 330,000, not 800,000, differently every run. The fix here is not sharing: one slot per goroutine. Locks come in the sync lesson.

Common mistakes

  • Using the loop variable inside the literal. Every goroutine sees the final index: repeated values, zeros elsewhere, or index out of range.
  • Printing before wg.Wait(), or calling wg.Add(1) inside the goroutine. Zeros: nothing was stored yet, or Wait saw a count of zero.
  • Passing the group by value. Done runs on a copy, so Wait hangs or the program panics.

Your exercise

Compute a result for every input number in its own goroutine and print the results in input order. The starter reads the input and prints; you decide how each goroutine gets its index and value, start them all, and wait before the printing begins. The _ = &wg and _, _ = i, v lines are placeholders that only silence "declared but not used" errors; delete them once you use those variables.

Up nextChannelsGoroutines and Channels

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…