Channels
Goroutines need a safe way to talk to each other: to hand over results, to pass work along, and to say "I am finished". Go's answer is the channel, a typed pipe. One goroutine sends a value in at one end, and another receives it at the other end. The Go proverb sums up the idea: do not communicate by sharing memory; share memory by communicating.
In this lesson you will create channels, send and receive, use buffered channels, close and range over channels, restrict channels to one direction, recognise deadlocks, and build a worker pool.
Sending and receiving
Create a channel with make(chan T), where T is the type of value it carries. The arrow operator <- does both jobs: ch <- v sends v, and v := <-ch receives a value.
package main
import (
"fmt"
"time"
)
func cook(dish string, ready chan string) {
time.Sleep(50 * time.Millisecond) // cooking takes time
ready <- dish + " is ready"
}
func main() {
ready := make(chan string)
go cook("Masala dosa", ready)
fmt.Println("Waiting for the kitchen...")
msg := <-ready // blocks until cook sends
fmt.Println(msg)
}Waiting for the kitchen... Masala dosa is ready
A channel made with make(chan string) is unbuffered: a send waits until a receiver takes the value, and a receive waits until a value is sent. The two goroutines meet at that moment, like a waiter handing a plate directly to a customer. That meeting also synchronises them, so here no WaitGroup is needed: main cannot finish before it receives the message.
Buffered channels
make(chan T, n) creates a buffered channel that can hold up to n values. Sends only wait when the buffer is full, and receives only wait when it is empty. len gives the number of values waiting and cap the buffer size.
package main
import "fmt"
func main() {
tokens := make(chan int, 3)
tokens <- 101
tokens <- 102
tokens <- 103 // buffer is now full
fmt.Println(len(tokens), cap(tokens))
fmt.Println(<-tokens, <-tokens)
fmt.Println(len(tokens))
}3 3 101 102 1
A channel is first in, first out, like a queue at a clinic counter: 101 comes out first. Buffers smooth out bursts, for example letting a producer run a little ahead of a slower consumer. They do not replace proper synchronisation; choose the size for a reason, not to "fix" a deadlock.
Closing and ranging
A sender can close(ch) to say "no more values are coming". Receivers can then loop with for v := range ch, which stops automatically after the last value. A receive on a closed, empty channel returns the zero value at once; the comma ok form tells you the channel is closed:
package main
import "fmt"
func generateOrders(n int, out chan<- string) {
for i := 1; i <= n; i++ {
out <- fmt.Sprintf("order-%d", i)
}
close(out)
}
func main() {
orders := make(chan string)
go generateOrders(3, orders)
for o := range orders {
fmt.Println("Packing", o)
}
fmt.Println("All orders packed")
v, ok := <-orders
fmt.Printf("%q %v\n", v, ok)
}Packing order-1 Packing order-2 Packing order-3 All orders packed "" false
Rules for closing: only the sender closes a channel, never the receiver, and only once. Sending on a closed channel panics with send on closed channel. You do not have to close every channel; close one only when receivers need to know that the stream has ended, as range does.
Directional channel types
The parameter type chan<- string in generateOrders means "this function may only send". The opposite, <-chan string, means "may only receive". A normal chan string converts to either automatically when you pass it. Directional types document intent, and the compiler rejects a receive from a send-only channel, so mistakes are caught early.
Deadlocks
If every goroutine is waiting on a channel and nobody can proceed, the program is stuck. Go detects the case where all goroutines are blocked and stops with a clear error:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1 // waits forever: nobody is receiving
fmt.Println(<-ch)
}fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main()
The output continues with the file name and line number. The send on an unbuffered channel needs a receiver in another goroutine, but main is the only goroutine and it is stuck at the send. Common causes are forgetting to start the goroutine, forgetting to close a channel that someone ranges over, or sending more values than anyone receives.
A worker pool
A worker pool is a fixed number of goroutines taking jobs from one channel and sending results to another. It limits how much work runs at once, for example so a program does not open 10,000 database connections. Here three workers calculate 18% GST on five invoices:
package main
import (
"fmt"
"slices"
"sync"
)
func worker(jobs <-chan int, results chan<- int) {
for amount := range jobs {
results <- amount * 18 / 100
}
}
func main() {
invoices := []int{1000, 2500, 400, 1200, 900}
jobs := make(chan int)
results := make(chan int)
var wg sync.WaitGroup
for range 3 {
wg.Go(func() { worker(jobs, results) })
}
go func() {
for _, amt := range invoices {
jobs <- amt
}
close(jobs) // workers' range loops end
}()
go func() {
wg.Wait()
close(results) // all workers done, so results can close
}()
var taxes []int
for t := range results {
taxes = append(taxes, t)
}
slices.Sort(taxes)
fmt.Println(taxes)
total := 0
for _, t := range taxes {
total += t
}
fmt.Println("Total GST:", total)
}[72 162 180 216 450] Total GST: 1080
Follow the closing chain: the feeder closes jobs when all invoices are sent, each worker's range jobs then ends, wg.Wait() returns, and a helper goroutine closes results, which ends the range results loop in main. Results arrive in whatever order the workers finish, so the program sorts them before printing to get the same output every time.
Common mistakes
- Sending with no receiver on an unbuffered channel. The sender blocks forever; start the receiver first or in another goroutine.
- Forgetting to close a channel that is ranged over. The
rangeloop waits forever. - Closing from the receiver side, or twice. Only the sender closes, exactly once.
- Expecting results in the order jobs were sent. With several workers, order is random; sort or index the results.
Key points
make(chan T)creates an unbuffered channel;ch <- vsends and<-chreceives.- Unbuffered channels hand values over directly and synchronise both goroutines; buffered channels hold up to
nvalues. - The sender closes a channel;
range chreads until it is closed;v, ok := <-chreports closure. chan<- Tis send-only and<-chan Tis receive-only.- When all goroutines are blocked, Go stops with "all goroutines are asleep - deadlock!".
Exercise
Build a two-stage pipeline for a Rohtak school. Stage one sends the marks 55, 82, 38, 91, 67 into a channel and closes it. Stage two reads marks, adds 5 grace marks to each (maximum 100), and sends them to a second channel, closing it at the end. main ranges over the second channel and prints each new mark and the count of students with 60 or more.
Show solution
package main
import "fmt"
func produce(marks []int) <-chan int {
out := make(chan int)
go func() {
for _, m := range marks {
out <- m
}
close(out)
}()
return out
}
func addGrace(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for m := range in {
out <- min(m+5, 100)
}
close(out)
}()
return out
}
func main() {
count := 0
for m := range addGrace(produce([]int{55, 82, 38, 91, 67})) {
fmt.Println(m)
if m >= 60 {
count++
}
}
fmt.Println("60 or more:", count)
}60 87 43 96 72 60 or more: 4
Each stage owns its output channel and closes it when its input runs out. With one goroutine per stage, values keep their order through the pipeline.
Quick Quiz
1. What happens when you send on an unbuffered channel and no goroutine is receiving?
An unbuffered send waits for a matching receive; if none can ever come, it is a deadlock.
2. Who should close a channel?
Only senders close channels; receiving from a closed channel is safe, sending on one panics.
3. What does v, ok := <-ch give when ch is closed and empty?
Receiving from a closed, drained channel returns immediately with the zero value and ok == false.
4. What does the type <-chan int mean?
The arrow before chan means values only come out of it.
Learn Go with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
