skip to content

Closing Channels and range

Closing is how a sender says "no more values", and receivers see it through the comma-ok form or a range loop that simply ends. The rules interviewers test are that only the sender closes, that sending on or re-closing a closed channel panics, and that a closed channel keeps yielding zero values forever.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does close(ch) do to a Go channel, and what do receivers see afterwards?

level: juniorimportance: must knowfreq 82%

answer

  1. a signal, not a cleanup
  2. buffered values still arrive first
  3. what ends a for-range over a channel
  4. the second return value distinguishes them
  5. zero value with ok false, every time

basics

~20 s

close(ch) marks a Go channel as finished. Receives stop blocking: they deliver any values still buffered, then return the element type's zero value with ok false, and a for range loop over the channel exits.

solid answer

~50 s

`close(ch)` records that no more values will ever be sent. It does not discard anything: values already buffered are still delivered in order, and only once the channel is drained does every receive return immediately with the element type's zero value. The two-value form `v, ok := <-ch` reports that state — `ok` is `false` exactly when the channel is closed and empty, which is the only way to distinguish "someone sent 0" from "there is nothing more". A `for v := range ch` loop receives until the channel is closed and drained, then exits; it never hands you the trailing zero value. A closed channel stays closed and is permanently ready to receive, so a `select` case receiving from it will be chosen every time. Closing is a signal to receivers, not a cleanup step, and it is done by the sending side.

code

go · 9 lines
go
ch := make(chan string, 1)
ch <- "one"
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // prints: one true

v, ok = <-ch
fmt.Println(v == "", ok) // prints: true false

go deeper

for a junior

Be ready to say what each of the three receive forms gives you once a channel is closed, and that the sending side is the one that calls close.

for a middle

Explain the ordering out loud: buffered values drain first, and only then does every receive return the zero value with ok false, permanently and immediately.

for a senior

Show that you reach for the comma-ok form whenever the zero value is a legal payload, and that you know a closed channel makes a select case ready on every pass.

for a principal

Treat "is this channel ever closed, and by whom" as part of a package's published contract. Callers write range loops against that promise and you cannot quietly withdraw it later.

## What `close` actually is `close` is a Go builtin that takes one channel argument. Calling `close(ch)` records, in the channel's runtime state, a single fact: **no further values will ever be sent on this channel**. That is all it does. It does not free memory, does not stop any goroutine, does not empty the buffer, and does not wake senders (there must not be any left — see the panics below). Because it is a statement about the channel as a whole rather than about the goroutine calling it, `close` belongs to the **sending side**. The receiving side never knows whether more values are coming; only a sender does. ## What a receiver observes There are three receive forms, and closing changes all three in the same way. **1. `v := <-ch`** — before the close, this blocks until a value arrives. After the channel is closed *and drained*, it returns the zero value of the element type immediately and never blocks again: `0` for `int`, `""` for `string`, `nil` for a pointer, slice, map or interface, and the all-zero struct for a struct type. **2. `v, ok := <-ch`** — the comma-ok form. `ok` is `true` when the value was genuinely sent by someone, and `false` when the receive completed only because the channel is closed and empty. This is the *only* way to tell a real sent zero value apart from end-of-stream. If your element type is `int` and a producer legitimately sends `0`, a bare `v := <-ch` cannot distinguish the two cases. **3. `for v := range ch`** — receives repeatedly and terminates when the channel is closed and drained. It never yields the trailing zero value; the close is the loop's exit condition, and no `break` is needed. ## Ordering: buffered values come first A common misconception is that closing discards whatever is still sitting in a buffered channel. It does not. Given `ch := make(chan string, 3)` with three values in it, closing and then receiving four times yields the three real values with `ok == true`, and only the fourth receive returns `""` with `ok == false`. The closed state becomes visible to receivers only after the buffer is empty. ## The closed state is permanent and always ready Once closed, a channel is closed forever; there is no reopen. Every subsequent receive succeeds immediately. Two consequences matter in practice: - A `select` case that receives from a closed channel is *always* ready, so a loop that keeps selecting on it will spin at full speed unless you stop selecting on that case. - Because *every* waiting receiver is released at once, closing is Go's broadcast primitive: one `close` wakes an arbitrary number of goroutines blocked on a receive, which is why cancellation is usually signalled by closing a `chan struct{}` rather than by sending a value per waiter. ## What `close` is not It is not a resource release. A channel is an ordinary garbage-collected value; nothing is freed by closing it, and nothing leaks by not closing it. It is not a flush, not a disconnect, and not an acknowledgement that receivers have finished reading — `close` returns immediately and tells you nothing about the other side. ## Who is allowed to close By convention and by compiler enforcement at the edges: the sender closes, the receiver never does. A parameter declared as a receive-only channel (`<-chan T`) cannot be closed at all — the compiler rejects it — so writing the direction into a function signature turns the convention into a checked rule. ## Typical shape A producer goroutine sends everything it has, closes the channel with `defer close(out)` or as its last statement, and the consumer drives a `for v := range out` loop that ends by itself. The producer never needs a sentinel value, and the consumer never needs a count.

  • With an element type of int, how do you tell a genuinely sent 0 apart from a closed channel?
    Use the comma-ok form: `v, ok := <-ch`. When a producer actually sent `0` you get `0, true`; when the channel is closed and drained you get `0, false`. The bare `v := <-ch` form throws that distinction away, which is why any protocol whose zero value is a legal payload must use comma-ok or a `for range` loop.
  • Does closing a buffered channel that still holds values discard them?
    No. Closing only records that nothing more will be sent. Receivers still get every buffered value, in FIFO order, with `ok == true`; only after the buffer is empty does a receive return the zero value with `ok == false`. So a producer may safely fill a buffered channel and close it immediately — the consumer sees all of it.
  • Why is a receive from a closed channel useful inside a select?
    A closed channel is permanently ready to receive, so every goroutine blocked on `<-done` is released the instant `done` is closed. That makes `close` a one-to-many broadcast that no number of receivers can miss. The flip side: once closed, that case is ready on every pass, so a loop must stop selecting on it or it will spin.

Closing a channel is like hanging a "no more deliveries" sign on a loading dock. Crates already on the dock are still collected first; after that, anyone who checks is told at once that nothing more is coming, and the answer never changes.

saying these in an interview costs you the question

  • Says receiving from a closed channel panics
  • Thinks close discards values still sitting in the buffer
  • Cannot distinguish a sent zero value from end-of-stream
  • Believes a for-range over a channel yields one final zero value
  • Calls close to free the channel's memory
open as a page

Which Go channel operations panic once the channel is closed or nil?

level: middleimportance: must knowfreq 66%

basics

~10 s

Three operations panic: sending on a closed Go channel, closing an already-closed channel, and closing a nil channel. Receiving from a closed channel never panics; it returns the zero value immediately with ok false.

open as a page

Several parser goroutines each close the shared records channel and it panics with "send on closed channel". How do you fix it?

level: seniorimportance: should knowfreq 57%

basics

~20 s

close describes the whole channel, not one sender's share of it, so with several senders none may close. Let the single owner that started them close once they have all returned, or leave the data channel open and close a separate done channel instead.

open as a page

Must every Go channel be closed, and does an unclosed one leak?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

No. Closing a Go channel is a signal to receivers, not a resource release, and an unreachable channel is garbage collected whether it was closed or not. Close only when receivers must learn that no more values are coming.

open as a page