EthicsComputerCourses
Join training +91 90532 10052

Channels

Go · Lesson 23 of 43 Advanced 7 min read Updated

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.

Go
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)
}
Output
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.

Go
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))
}
Output
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:

Go
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)
}
Output
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:

Go
package main

import "fmt"

func main() {
	ch := make(chan int)
	ch <- 1 // waits forever: nobody is receiving
	fmt.Println(<-ch)
}
Output
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:

Go
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)
}
Output
[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 range loop 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 <- v sends and <-ch receives.
  • Unbuffered channels hand values over directly and synchronise both goroutines; buffered channels hold up to n values.
  • The sender closes a channel; range ch reads until it is closed; v, ok := <-ch reports closure.
  • chan<- T is send-only and <-chan T is 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
Go
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)
}
Output
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?

2. Who should close a channel?

3. What does v, ok := <-ch give when ch is closed and empty?

4. What does the type <-chan int mean?

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