Channels
Go has a well-known slogan for how it wants you to handle concurrency: "Do not communicate by sharing memory; instead, share memory by communicating." Channels are the mechanism that makes that possible.
Creating and using a channel
ch := make(chan int)
go func() {
ch <- 42 // send 42 into the channel
}()
value := <-ch // receive from the channel
fmt.Println(value) // 42chan int is a channel that carries int values. ch <- 42 sends; <-ch receives. Both directions block: a send waits until something is ready to receive, and a receive waits until something sends. That blocking is the actual synchronization mechanism — it's what makes value := <-ch in the example above safe to read immediately, without any separate wg.Wait(): the receive simply doesn't proceed until the goroutine has sent.
Why this beats a shared variable with a lock
Contrast with the alternative: two goroutines both reading and writing the same variable, coordinated with a mutex. That works too (Go has sync.Mutex for exactly that), but it requires every piece of code that touches the shared variable to remember to lock and unlock correctly, forever, including code added later by someone who didn't see the original design. A channel makes the handoff itself the shared thing — ownership of the data moves from sender to receiver at the moment of the send, and only one goroutine holds it at a time. There's nothing to forget to lock, because there's no shared variable left to protect.
Buffered channels
make(chan int) above is unbuffered — a send blocks until a receive is ready, right that moment. A buffered channel accepts a fixed number of values without an immediate receiver:
ch := make(chan int, 2)
ch <- 1 // doesn't block — buffer has room
ch <- 2 // doesn't block — buffer now full
ch <- 3 // blocks — buffer is full, waits for a receive to free a slotUse buffering when a producer can legitimately get a little ahead of its consumer; leave a channel unbuffered by default when you specifically want the "handed off, guaranteed" synchronization an unbuffered send/receive gives you.
Closing a channel and ranging over it
A sender can close(ch) to signal "nothing more is coming." Receivers can detect this, and range over a channel until it's closed:
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
// prints 1, 2, 3, then the loop ends when ch is closedOnly the sender should close a channel, never the receiver — closing a channel you're only receiving from (or closing one twice) panics.