Goroutines
Concurrency is where Go stops looking like other languages. The unit of concurrency is the goroutine: a function running independently of the one that started it. Starting one takes a single keyword:
package main
import (
"fmt"
"sync"
)
func greet(name string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Hello,", name)
}
func main() {
var wg sync.WaitGroup
for _, name := range []string{"Ada", "Grace", "Ken"} {
wg.Add(1)
go greet(name, &wg)
}
wg.Wait()
}
go greet(...) starts greet in a new goroutine and immediately moves on —
main does not wait for it. The three greetings may print in any order,
because the Go runtime schedules goroutines however it likes.
Why goroutines are cheap
A goroutine is not an operating-system thread. It starts with a tiny stack
(a few kilobytes) that grows and shrinks as needed, and the Go runtime
multiplexes thousands of goroutines onto a handful of OS threads. Spawning a
goroutine per item, per request, or per connection is normal Go — where other
languages reach for thread pools, Go just says go.
main waits for nobody
There is one brutal rule: when main returns, the program exits, killing
every goroutine mid-sentence. This program probably prints nothing:
func main() {
go fmt.Println("you will likely never see this")
}
main reaches its end before the scheduler ever runs the new goroutine.
Sleeping "long enough" is a bug, not a fix — the honest answer is to wait
for the work to finish.
Coordinating with WaitGroup
That's what sync.WaitGroup did in the first example. It's a counter:
Add(1) before starting each goroutine, Done() (usually deferred) when one
finishes, and Wait() blocks until the counter hits zero. We'll study it
properly in the Coordination chapter; for now it's the standard way to say
"start N goroutines, then wait for all N."
Note the order: Add happens before go. If the goroutine itself called
Add, Wait might run before the counter ever rose.
Sharing data, carefully
Goroutines run in the same address space, so they can read and write the same variables — and two goroutines writing the same variable at the same time is a data race, one of the nastiest bugs in concurrent code. Go's proverb is: "Don't communicate by sharing memory; share memory by communicating." The next lesson introduces channels, the tool that proverb is about.
There is a useful exception, and the exercise below uses it: if each
goroutine writes to a different location — say, its own index of a shared
slice — there is no race, because nobody touches the same memory. One
goroutine per element, each filling in result[i] for its own i, then a
WaitGroup to wait for them all: that pattern is safe, fast, and very Go.
Give it a try.
Practice
1. Parallel Double
IntermediateImplement
parallelDoubleso it doubles every element of a slice — using one goroutine per element:func parallelDouble(nums []int) []int- Create the result slice first, then spawn one goroutine per element; each
goroutine writes
nums[i] * 2intoresult[i]— its own index, nobody else's. - Coordinate with a
sync.WaitGroupso the function only returns after every goroutine has finished. - The result order must match the input order:
parallelDouble([]int{1, 2, 3})returns[]int{2, 4, 6}. - An empty (or nil) input returns an empty slice.
Yes, a plain loop would be simpler — the point here is the pattern: pre-size the output, fan out one goroutine per independent piece of work, wait for all of them. You'll reuse it constantly with real work in place of
* 2.package main import "fmt" // parallelDouble returns a new slice where each element is twice the // corresponding element of nums, computed by one goroutine per element. func parallelDouble(nums []int) []int { // TODO: make the result slice, spawn one goroutine per element, // and wait for them all with a sync.WaitGroup. return nil } func main() { fmt.Println(parallelDouble([]int{1, 2, 3})) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Create the result slice first, then spawn one goroutine per element; each
goroutine writes