Goroutines and channels
Launch concurrent tasks with goroutines and communicate between them safely with channels.
- Launch a goroutine with the go keyword
- Explain the goroutine lifecycle and what happens when main returns
- Create buffered and unbuffered channels with make
- Send and receive values on a channel
- Close a channel and range over it to drain all values
Go was designed for concurrency from the start. The language primitives — goroutines and channels — are built into the runtime, not bolted on as a library. Understanding them is not optional for production Go work: almost every non-trivial service uses them, and Go's standard library assumes you know the model.
The guiding slogan: "Don't communicate by sharing memory; share memory by communicating." Channels are the mechanism for that communication.
Goroutines
A goroutine is a lightweight, concurrently executing function. The go keyword launches one:
go func() {
fmt.Println("running in a goroutine")
}()Goroutines are much cheaper than OS threads — a new goroutine starts with a few kilobytes of stack. A Go program can run hundreds of thousands of goroutines simultaneously.
Important: when main returns, all goroutines are killed immediately, including ones still running. If you launch a goroutine in main and return before it finishes, its work is lost.
func main() {
go fmt.Println("this may not print")
// main returns immediately, goroutine may not get scheduled
}You need a synchronisation mechanism to wait. Channels are one way; sync.WaitGroup (covered later) is another.
Channels
A channel is a typed conduit for sending values between goroutines. Create one with make:
ch := make(chan int) // unbuffered
bch := make(chan int, 5) // buffered, capacity 5Send a value with <-:
ch <- 42 // send 42 into chReceive a value:
v := <-ch // receive from ch, store in vAn unbuffered channel blocks the sender until a receiver is ready, and blocks the receiver until a sender sends. This synchronises the two goroutines at every exchange.
func main() {
ch := make(chan string)
go func() {
ch <- "hello from goroutine"
}()
msg := <-ch
fmt.Println(msg) // hello from goroutine
}The main goroutine blocks on <-ch until the other goroutine sends. This is how you wait for a goroutine to produce a result.
Buffered channels
A buffered channel can hold up to its capacity without a receiver waiting:
ch := make(chan int, 3)
ch <- 1 // does not block — buffer has room
ch <- 2
ch <- 3
// ch <- 4 would block here — buffer is fullBuffered channels decouple sender and receiver. Use buffered channels when you know roughly how many items the producer will generate before the consumer catches up — for example, a worker queue.
An unbuffered channel provides synchronisation: sender and receiver meet at the exchange point. A buffered channel provides decoupling: the sender can proceed up to the capacity limit without waiting. Neither is universally better — choose based on whether you want the sender to wait.
Closing a channel
A sender can close a channel to signal that no more values will be sent:
close(ch)Receivers can detect a close with the comma-ok pattern:
v, ok := <-ch
if !ok {
// channel is closed and empty
}More commonly, range over a channel drains it until close:
for v := range ch {
fmt.Println(v) // receives each value until ch is closed
}Only the sender should close a channel. Closing a channel from the receiver side, or closing it twice, panics. If multiple goroutines might send, coordinate with a separate done signal or use a sync.Once to close exactly once.
A simple pipeline
Channels compose naturally into pipelines:
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
for v := range square(generate(2, 3, 4)) {
fmt.Println(v) // 4, 9, 16
}
}Each stage is independent. The goroutines run concurrently — generate sends while square computes and main prints.
Check your understanding
Knowledge check
- 1.You send to an unbuffered channel but no goroutine is receiving. What happens?
- 2.It is safe for any goroutine (sender or receiver) to call close() on a channel.
- 3.A goroutine sends values into a channel and closes it when done. What is the idiomatic way to receive all values in the consumer?
Do it yourself
Write a fanOut function that sends the integers 1 to 10 into a channel, then read and print them in main using a range loop. Add a second goroutine that squares each value as it arrives.
go run main.go
go vet ./...Where to go next
You have goroutines and channels. The next lesson covers the select statement — how to wait on multiple channels simultaneously, handle timeouts, and build non-blocking operations.