Goroutines and channels
โ Report an issue with this lessonfunc fetchPrice(course string, results chan<- string) {
// pretend this takes some time
results <- course + ": $29.99"
}
func main() {
results := make(chan string)
go fetchPrice("HTML Fundamentals", results)
go fetchPrice("Python Basics", results)
fmt.Println(<-results)
fmt.Println(<-results)
}
go starts a function running concurrently as a
goroutine -- cheap enough that Go programs commonly run thousands of them.
Channels are the safe way for goroutines to communicate, instead of
sharing memory directly.
Goroutines are cheap because they aren't OS threads. Each one starts with a small (a few KB), growable stack managed by the Go runtime, which multiplexes many goroutines onto a much smaller number of actual OS threads. Spawning a goroutine costs closer to allocating a small object than to creating a thread, which is why "just start a goroutine for it" is a normal, everyday tool in Go rather than something you reach for cautiously.
By default, channels are unbuffered: a send blocks until another
goroutine is ready to receive, and a receive blocks until a value
arrives. This is a real synchronization point, not just a queue --
results <- value completing tells you the receiver has
already taken the value, not merely that it was queued somewhere. A
buffered channel relaxes that: make(chan string, 3) lets up
to 3 sends complete without a waiting receiver, only blocking once the
buffer fills:
results := make(chan string, 2)
results <- "a" // doesn't block -- buffer has room
results <- "b" // doesn't block -- buffer now full
// results <- "c" // would block until something reads from results
A closed channel is how a sender tells receivers "no more values are
coming." Reading from a closed channel never blocks -- it immediately
returns the zero value, plus a second boolean that's false
once the channel is drained and closed:
results := make(chan int)
go func() {
for i := 0; i < 3; i++ {
results <- i
}
close(results) // signal: no more values
}()
for v := range results { // range exits automatically when the channel closes
fmt.Println(v)
}
Only the sender should ever close a channel -- closing a channel you
receive from (or closing an already-closed channel) panics. A common
gotcha: forgetting to close a channel a range loop is
reading from causes that loop to block forever once all values have
been consumed, since range has no other way to know the
sequence is finished.
When you need to wait on multiple channels at once -- a result
channel and a cancellation signal, say -- select picks
whichever case is ready first, and a default case makes it
non-blocking:
select {
case v := <-results:
fmt.Println("got:", v)
case <-time.After(2 * time.Second):
fmt.Println("timed out")
default:
fmt.Println("nothing ready yet")
}
Try it yourself
HTML Fundamentals: $29.99 Python Basics: $29.99
Run your code and get it working before marking this lesson complete.
9 more lessons โ including Project: build and deploy a courses API โ and a final exam plus a certificate are waiting.
Unlock the full course โ $99.99