liveBook · Manning

Goroutine

As explained in 100 Go Mistakes and How to Avoid Them

A goroutine is a lightweight independently executing function managed by the Go runtime.

A goroutine lets a function run concurrently with its caller. Starting one is easy; deciding when it finishes, what variables it reads, and whether it improves performance takes more care. section 1.2 section 9.2

A common mistake is to start a goroutine without an exit plan. A goroutine receiving in `for v := range ch` exits when `ch` closes. If the function supplying `ch` never closes it, the goroutine remains. A leaked goroutine can retain heap references and resources such as connections or open files. section 9.2

Choose an exit mechanism that matches who owns the work:

  • For a watcher whose work should end on cancellation, pass it a context and have its watch operation close its resources when that context is canceled. section 9.2
  • If the caller owns a watcher, return it with a close operation and arrange for the caller to invoke that operation. section 9.2

A closure reads a changing loop variable

Consider `for _, i := range s { go func() { fmt.Print(i) }() }` for `s := []int{1, 2, 3}`. In this example, the closures refer to the same `i`; they do not preserve its value when each goroutine starts. The output can therefore differ from the expected values, and completion order is not guaranteed. Give each invocation its value instead: `for _, i := range s { go func(val int) { fmt.Print(val) }(i) }`. This fixes which value each function prints, not the order of printing. section 9.3

Concurrency also needs coordination. In parallel merge sort, two goroutines sort separate halves of a slice; the parent calls `Wait` on a `sync.WaitGroup` before merging them. Call `Add` before starting the goroutines so the group accounts for the work they will perform. section 8.2.2 section 9.15

const max = 2048                      #1
 
func parallelMergesortV2(s []int) {
    if len(s) <= 1 {
        return
    }
 
    if len(s) <= max {
        sequentialMergesort(s)        #2
    } else {                          #3
        middle := len(s) / 2
 
        var wg sync.WaitGroup
        wg.Add(2)
 
        go func() {
            defer wg.Done()
            parallelMergesortV2(s[:middle])
        }()
 
        go func() {
            defer wg.Done()
            parallelMergesortV2(s[middle:])
        }()
 
        wg.Wait()
        merge(s, middle)
    }
}

The threshold in this implementation keeps small merge-sort sublists sequential instead of starting goroutines at every split. section 8.2.2

More goroutines do not automatically mean a faster program. On a four-core benchmark with 10,000 elements, merge sort that starts goroutines recursively down to tiny sublists runs much slower than the sequential version. A version that sorts sublists of at most 2,048 elements sequentially performs better in that benchmark. Use parallel work where its size justifies starting and coordinating goroutines. section 8.2.2

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Works through code and observable effects for leaks, closure capture, and merge-sort performance without detailing runtime scheduling.
Examples
Contrasts leaking channel receivers and watcher shutdown options, then uses loop output and merge-sort benchmarks to show other pitfalls.
What it assumes
Recaps closure behavior and briefly defines sync.WaitGroup rather than requiring prior mastery of either.
What you can do after
Diagnose/debug goroutine leaks and loop-variable surprises, and evaluate when parallel work warrants goroutine overhead.
How it connects
Connects goroutines to channels, contexts, closures, loop variables, sync.WaitGroup, and data races.

Also explained in

Compare how 7 books explain goroutine →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.