GoByte Skills Episode 1 of 27, track Go (1 of 3)

Go channels: the queue that says no

GoByte Skills #1: A buffered channel is a bounded queue. When it is full, the send parks until there is room, so the slow consumer sets the pace. Backpressure in one argument to make(), plus the exact order a real run prints.

Piere is one of GoByte's characters. This post was drafted by AI agents in Piere's voice, then fact checked, run and edited by the GoByte team.

Back to top

A channel made with make(chan T, n) is a FIFO queue with room for n values. A full channel neither fails a send nor grows: the sender parks until a receiver makes room. That is backpressure, in one argument to make.

Animation for GoByte Skills #1: Tokens fill a two slot pipe and the sender waits.
Transcript

The Go program types in while numbered tokens move in the order a real run prints them: token 1 goes straight to the waiting consumer, 2 and 3 fill the two slots, 4 parks at the full channel until the consumer takes 2, and after close the buffered 4 and 5 still drain before range stops.

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 2)
	go func() {
		defer close(ch)
		for i := 1; i <= 5; i++ {
			ch <- i // parks while full
			fmt.Println("sent", i, "queued", len(ch))
		}
	}()
	for v := range ch {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("got ", v)
	}
}
sent 1 queued 0
sent 2 queued 1
sent 3 queued 2
got  1
sent 4 queued 2
got  2
sent 5 queued 2
got  3
got  4
got  5

Why this order#

range is already waiting when 1 is sent, so the runtime hands 1 straight over and the buffer stays empty. While the consumer sleeps, 2 and 3 fill both slots and the send of 4 parks. When range takes 2, the runtime moves 4 from the parked sender into the freed slot and wakes it. Line one depends on who arrives first, the rest on the sleep.

The exact bound#

len(ch) never exceeds cap(ch). The producer is at most n + 2 values ahead of finished work: n buffered, one in the consumer's hands, one parked in the send (above: 1, then 2 and 3, then 4).

What waiting costs#

A parked goroutine is off the run queue, so it burns no CPU. It still keeps its stack (2 KiB minimum) and its pending value. Ten thousand parked senders are an unbounded queue built from goroutines.

Why not a bigger buffer#

A buffer absorbs bursts; it cannot fix a rate gap. If the producer is faster on average, every finite buffer fills, and an unbounded one (a growing slice) trades latency for memory until the process dies. Capacity is also latency: a value entering a full buffer waits for the n ahead of it plus the one in progress.

Closing#

Only the sender closes: a send on a closed channel panics. Receives still work: buffered values come first, then the zero value with ok == false. Buffer 1 and 2, close, receive three times: 1 true, 2 true, 0 false. So range still drains 4 and 5 after close (3 was already taken), then stops.

Rule of thumb: size the buffer for a measured burst, and let a full channel do the refusing. A queue that never says no is a memory leak with good manners.

Report a mistake

Your product here? Partner with us

Back to top

Discussion

No comments yet. Signed in GoByte members with a verified e-mail can join. Community guidelines

Reading is open to everyone. Commenting and voting need a GoByte account with a verified e-mail.