Channels
Goroutines are easy to start; the hard part is getting data between them safely. Go's answer is the channel: a typed pipe that one goroutine sends into and another receives from.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "ping" // send
}()
msg := <-ch // receive
fmt.Println(msg)
}
make(chan string) creates a channel of strings. The arrow shows data flow:
ch <- v sends v into the channel, v := <-ch receives from it. A value
sent by one goroutine is handed off to the receiver — no shared variable,
no race. This is what "share memory by communicating" means in practice.
Sends and receives block
By default a channel is unbuffered, and both operations block: a send waits until some goroutine is ready to receive, and a receive waits until some goroutine sends. The two goroutines meet at the channel — a rendezvous — and the value changes hands at that moment.
That blocking is a feature. In the example above, main parks at <-ch
until the anonymous goroutine delivers — the channel is doing the
synchronization that would otherwise need a WaitGroup.
Deadlock
Blocking also means you can wedge a program solid. This one crashes:
func main() {
ch := make(chan int)
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
}
main sends, but no other goroutine exists to receive, so main blocks
forever. The runtime notices that every goroutine is stuck and kills the
program with a deadlock error. The rule of thumb: an unbuffered send needs a
receiver already running (or about to run) on the other side — which usually
means the send belongs inside a goroutine.
Closing a channel
A sender can signal "no more values" by closing the channel:
close(ch)
Receiving from a closed channel never blocks: it returns the zero value immediately, and the two-value form tells you whether the channel is still open:
v, ok := <-ch // ok == false: channel closed and drained
Two rules: only the sender closes (never the receiver), and closing is optional — it's only needed when receivers must know the stream has ended.
Ranging over a channel
The most common reason to close is so a receiver can range:
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v) // 1, 2, 3 — then the loop ends
}
}
for v := range ch receives until the channel is closed and drained. Forget
the close, and the loop blocks forever waiting for a value that never comes
— your first real-world deadlock will almost certainly be a missing close.
This producer pattern — a goroutine that sends a sequence and closes — is called a generator, and building one is exactly your exercise.
Practice
1. Number Generator
IntermediateImplement
generateso it returns a channel that yields the numbers1throughn, in order, and is then closed:func generate(n int) <-chan int- Create the channel, start a goroutine that sends
1, 2, ..., nand then callsclose, and return the channel right away — the caller receives the values as your goroutine produces them. - For
n <= 0, the channel yields nothing: it is just closed. - Callers must be able to write
for v := range generate(5)and see1 2 3 4 5, with the loop terminating.
The starter returns an already-closed empty channel so the code compiles — your job is to put a producing goroutine behind it. Remember: the sends belong inside the goroutine, and the sender is responsible for the
close.package main import "fmt" // generate returns a channel that yields 1..n in order and is then closed. func generate(n int) <-chan int { // TODO: send 1..n from a goroutine, then close the channel. ch := make(chan int) close(ch) return ch } func main() { for v := range generate(5) { fmt.Println(v) } }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Create the channel, start a goroutine that sends