43. Channels
A channel is a typed pipe between goroutines. One sends, another receives, and the channel handles the synchronisation. Go's slogan for this:
Don't communicate by sharing memory; share memory by communicating.
Read that as a preference, not a prohibition. Shared state behind a
sync.Mutex is the right answer often enough that it gets its own lesson later
in this module. The split in practice: channels move data between
goroutines; a mutex guards data that several goroutines touch. Pick
whichever makes the ownership easier to see.
Sending and receiving
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "hello from the goroutine"
}()
msg := <-ch
fmt.Println(msg)
}
The arrow always points in the direction the data moves:
ch <- v— sendvintochv := <-ch— receive fromchintov
The channel is typed: a chan string carries strings and nothing else.
Unbuffered channels synchronise
That example has no WaitGroup, no sleep, and no race. Here's why:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
fmt.Println("goroutine: sleeping before sending")
time.Sleep(20 * time.Millisecond)
fmt.Println("goroutine: sending 42")
ch <- 42
fmt.Println("goroutine: send completed")
}()
fmt.Println("main: waiting to receive")
v := <-ch
fmt.Println("main: received", v)
}
An unbuffered channel is a rendezvous. The sender blocks until a receiver is ready; the receiver blocks until a sender arrives. They meet, the value moves, and both continue.
That blocking is a feature, not a cost. It's how you get ordering guarantees
without locks: after <-ch returns, you know the sending goroutine reached
its send.
Channels as results
The natural way to get a value back out of a goroutine:
package main
import "fmt"
func sum(nums []int, out chan<- int) {
total := 0
for _, n := range nums {
total += n
}
out <- total
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8}
left := make(chan int)
right := make(chan int)
go sum(nums[:4], left)
go sum(nums[4:], right)
a, b := <-left, <-right
fmt.Println("halves:", a, b)
fmt.Println("total:", a+b)
}
Note the parameter type chan<- int — a send-only channel. The compiler
now rejects any attempt to receive from out inside sum. The mirror is
<-chan int for receive-only.
Directions cost nothing and document intent precisely: a function signature tells you whether it produces or consumes. Use them.
Closing a channel
A sender closes a channel to say "no more values are coming":
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
ch <- i * 10
}
close(ch)
}()
for v := range ch {
fmt.Println("received", v)
}
fmt.Println("channel closed, loop ended")
}
for v := range ch receives until the channel is closed, then ends. Without
the close, that loop would block forever waiting for a sixth value — a
deadlock, and Go's runtime would tell you so.
Three rules about closing:
- Only the sender closes. A receiver can't know whether more values are coming.
- Sending on a closed channel panics. Receiving from one is fine.
- Closing twice panics. So does closing a nil channel.
Most channels are never closed at all — closing matters when receivers use
range or need to know the stream ended.
Detecting a closed channel
Receiving from a closed channel returns the zero value immediately. The comma-ok form tells you whether that zero was real data:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 0
close(ch)
for i := 0; i < 4; i++ {
v, ok := <-ch
fmt.Printf("value=%d open=%t\n", v, ok)
}
}
The third line is the interesting one: value=0 open=false means "the
channel is drained and closed". The second line — value=0 open=true — was a
real zero somebody sent. Same value, different meanings, and comma-ok is what
separates them. (The same shape as map lookups and type assertions, for the
third time.)
Deadlocks
Go detects the case where every goroutine is blocked and crashes with a clear message rather than hanging:
func main() {
ch := make(chan int)
ch <- 1 // nobody is receiving
fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!
That's an unbuffered send with no receiver: main blocks, and there's no
other goroutine to rescue it. The common causes:
- sending with no receiver (or receiving with no sender)
- forgetting to
closea channel that something is ranging over - a
WaitGroupwhoseDoneis never reached - two goroutines each waiting for the other
The runtime only catches the case where all goroutines are stuck. A single leaked goroutine blocked forever is invisible — which is why the context lesson matters.
Channels of structs
Channels carry any type, and a struct is the usual choice for real work:
package main
import (
"fmt"
"sort"
)
type Result struct {
ID int
Value int
Err error
}
func process(id int, out chan<- Result) {
if id == 3 {
out <- Result{ID: id, Err: fmt.Errorf("item %d is cursed", id)}
return
}
out <- Result{ID: id, Value: id * 100}
}
func main() {
out := make(chan Result, 5)
for i := 1; i <= 5; i++ {
go process(i, out)
}
var results []Result
for i := 0; i < 5; i++ {
results = append(results, <-out)
}
sort.Slice(results, func(i, j int) bool { return results[i].ID < results[j].ID })
for _, r := range results {
if r.Err != nil {
fmt.Printf("%d: error: %v\n", r.ID, r.Err)
continue
}
fmt.Printf("%d: %d\n", r.ID, r.Value)
}
}
Carrying the error in the result is the standard way to report failure from a goroutine — a goroutine can't return anything to its starter, so the error travels with the value.
And note the sort: results arrive in whatever order the goroutines finish. If you need order, either sort at the end (as here) or use the one-slot-per-worker technique from the last lesson.
Your turn
Have a goroutine send the numbers 1 to 5 into a channel and close it; sum
them in main with a range loop:
15
package main
import "fmt"
func main() {
ch := make(chan int)
// start a goroutine that sends 1..5 and closes ch
sum := 0
for v := range ch {
sum += v
}
fmt.Println(sum)
}
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}()
sum := 0
for v := range ch {
sum += v
}
fmt.Println(sum)
}
Next: giving a channel a queue.