Knaph

Channels

Go has a well-known slogan for how it wants you to handle concurrency: "Do not communicate by sharing memory; instead, share memory by communicating." Channels are the mechanism that makes that possible.

Creating and using a channel

ch := make(chan int)
 
go func() {
	ch <- 42 // send 42 into the channel
}()
 
value := <-ch // receive from the channel
fmt.Println(value) // 42

chan int is a channel that carries int values. ch <- 42 sends; <-ch receives. Both directions block: a send waits until something is ready to receive, and a receive waits until something sends. That blocking is the actual synchronization mechanism — it's what makes value := <-ch in the example above safe to read immediately, without any separate wg.Wait(): the receive simply doesn't proceed until the goroutine has sent.

Why this beats a shared variable with a lock

Contrast with the alternative: two goroutines both reading and writing the same variable, coordinated with a mutex. That works too (Go has sync.Mutex for exactly that), but it requires every piece of code that touches the shared variable to remember to lock and unlock correctly, forever, including code added later by someone who didn't see the original design. A channel makes the handoff itself the shared thing — ownership of the data moves from sender to receiver at the moment of the send, and only one goroutine holds it at a time. There's nothing to forget to lock, because there's no shared variable left to protect.

Buffered channels

make(chan int) above is unbuffered — a send blocks until a receive is ready, right that moment. A buffered channel accepts a fixed number of values without an immediate receiver:

ch := make(chan int, 2)
ch <- 1 // doesn't block — buffer has room
ch <- 2 // doesn't block — buffer now full
ch <- 3 // blocks — buffer is full, waits for a receive to free a slot

Use buffering when a producer can legitimately get a little ahead of its consumer; leave a channel unbuffered by default when you specifically want the "handed off, guaranteed" synchronization an unbuffered send/receive gives you.

Closing a channel and ranging over it

A sender can close(ch) to signal "nothing more is coming." Receivers can detect this, and range over a channel until it's closed:

ch := make(chan int)
 
go func() {
	for i := 1; i <= 3; i++ {
		ch <- i
	}
	close(ch)
}()
 
for v := range ch {
	fmt.Println(v)
}
// prints 1, 2, 3, then the loop ends when ch is closed

Only the sender should close a channel, never the receiver — closing a channel you're only receiving from (or closing one twice) panics.

Sign in to track your progress through this course.

Ask an AI

Open a ready-made prompt in ChatGPT or Claude — just press Enter.

SummarizeChatGPTClaude
Ask me questionsChatGPTClaude
Let's learn togetherChatGPTClaude