Skip to content

Goroutines & Channels

Go’s concurrency model follows one guiding principle: “Don’t communicate by sharing memory; share memory by communicating.” Goroutines do the work, channels pass data between them safely.

A goroutine is a function running concurrently, started with go:

go doSomething()

Goroutines are cheap — a few KB of stack that grows as needed — so it’s normal to start thousands of them. The catch: main doesn’t wait for goroutines to finish on its own, so you need a way to synchronize.

A channel is a typed pipe for sending and receiving values between goroutines:

ch := make(chan string)
go func() {
ch <- "done" // send
}()
msg := <-ch // receive, blocks until a value arrives
fmt.Println(msg)

chan<- string and <-chan string are directional channel types — a nice bit of documentation-by-signature showing a function only sends or only receives.

When you don’t need to pass data back, just wait for goroutines to finish:

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("working", id)
}(i)
}
wg.Wait()

select waits on multiple channel operations at once, similar to a switch for channels:

select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
}