Channels in Go: Unbuffered, Buffered, close, range and select
Chapter 02 of 04

Channels in Go: Unbuffered, Buffered, close, range and select

Communicate between goroutines with channels, build a concurrent health checker, and implement timeouts with select and context.

Sabbir Ahmmed4 min read0
On this page

Goroutines run independently; channels let them talk. A channel is a typed pipe: one goroutine sends a value, another receives it. Go's motto summarises the philosophy: "Do not communicate by sharing memory; instead, share memory by communicating."

Sending and receiving

go
1
2
3
4
5
6
7
8
ch := make(chan int) // an unbuffered channel of ints

go func() {
	ch <- 10 // send
}()

v := <-ch // receive
fmt.Println(v) // 10

The arrow shows the direction data flows: ch <- v sends v into the channel, <-ch takes a value out.

Unbuffered channels synchronise

An unbuffered channel has no storage. A send blocks until a receiver is ready, and a receive blocks until a sender is ready. The handoff is a synchronisation point, like passing a baton:

handoff.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	ch := make(chan int)

	wg.Add(2)
	go func() {
		defer wg.Done()
		fmt.Println("sending...")
		ch <- 10 // waits here until the receiver arrives
	}()
	go func() {
		defer wg.Done()
		data := <-ch // waits here until a value is sent
		fmt.Println("received:", data)
	}()

	wg.Wait()
	fmt.Println("main goroutine done")
}
Warning
If a send has no receiver (or a receive has no sender), that goroutine blocks forever. If every goroutine is blocked, the runtime stops with fatal error: all goroutines are asleep - deadlock!.

Buffered channels

A buffered channel has capacity. Sends only block when the buffer is full; receives only block when it is empty.

go
1
2
3
4
jobs := make(chan int, 3)
jobs <- 1
jobs <- 2                      // no receiver needed yet: there is room
fmt.Println(len(jobs), cap(jobs)) // 2 3

Buffers smooth out bursts between a fast producer and a slower consumer. They do not fix design problems: if the consumer is permanently slower, a buffer only delays the moment everything blocks.

Closing and ranging

The sender closes a channel to say "no more values". Receivers can then drain it with range, which stops automatically after the last value:

go
1
2
3
4
5
6
7
close(jobs)
for j := range jobs {
	fmt.Print(j, " ") // 1 2
}

v, ok := <-jobs
fmt.Println(v, ok) // 0 false: closed and empty

The comma-ok form tells you whether the value is real (ok == true) or the zero value from a closed channel.

Rules: only the sender closes; sending on a closed channel panics; closing twice panics; you do not have to close a channel unless a receiver needs to know there is no more data.

Example: a website health checker

This is the link checker from the repository. Each URL is checked in its own goroutine, and results come back through a channel:

healthcheck.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"fmt"
	"net/http"
	"time"
)

type Result struct {
	URL    string
	Status string
}

func check(client *http.Client, url string, out chan<- Result) {
	resp, err := client.Get(url)
	if err != nil {
		out <- Result{url, "DOWN (" + err.Error() + ")"}
		return
	}
	resp.Body.Close()
	out <- Result{url, resp.Status}
}

func main() {
	links := []string{"https://go.dev", "https://google.com", "https://github.com"}
	client := &http.Client{Timeout: 5 * time.Second}
	results := make(chan Result)

	for _, link := range links {
		go check(client, link, results)
	}

	for range links { // receive exactly one result per link
		r := <-results
		fmt.Printf("%-22s %s\n", r.URL, r.Status)
	}
}

The total time is roughly the slowest single request, not the sum, because all checks run concurrently. The original version also re-checked each link forever every 5 seconds with for l := range ch { go func() { time.Sleep(5 * time.Second); check(l) }() }, which is a neat way to build a simple uptime monitor.

Notice the parameter type chan<- Result. That is a send-only channel: check cannot accidentally receive from it. Directional types (chan<- send-only, <-chan receive-only) document intent and are checked by the compiler.

Tip
Always set a timeout on HTTP clients. http.Get uses a client with no timeout, so one slow server can block a goroutine forever.

select: waiting on several channels

select waits until one of several channel operations can proceed. Combined with context, it is how you implement timeouts and cancellation:

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

slow := make(chan string)
go func() {
	time.Sleep(200 * time.Millisecond)
	slow <- "done"
}()

select {
case r := <-slow:
	fmt.Println(r)
case <-ctx.Done():
	fmt.Println("timeout:", ctx.Err()) // timeout: context deadline exceeded
}
Note
In this example the slow goroutine is still blocked on slow <- "done" forever after the timeout, which is a leak. Make the channel buffered (make(chan string, 1)) so the late send can complete and the goroutine can exit.

A select with a default case never blocks, which lets you try a send or receive without waiting.

Signalling with chan struct{}

When you only need "something happened", use chan struct{}. An empty struct takes zero bytes:

go
1
2
3
4
5
6
done := make(chan struct{})
go func() {
	defer close(done)
	// ... work ...
}()
<-done // wait until the worker closes the channel

Closing a channel wakes all receivers, so it is a great broadcast signal (for example, "shut down now").

Key takeaways

Channels are typed pipes between goroutines; ch <- v sends, <-ch receives.
Unbuffered channels synchronise sender and receiver; buffered channels decouple them up to their capacity.
The sender closes; receivers use range or v, ok := <-ch.
Use directional channel types in function signatures.
select plus context gives you timeouts and cancellation; watch for leaked senders.
#Golang#Concurrency#Channels

Last updated on

Discussion0