Grokking Concurrency in Go
Vote

0% completed

​

Closing a Channel and Ranging over It

Closing a Channel

The Comma-Ok Receive

Ranging over a Channel

A Range Loop with No Close

Only the Sender Closes

When to Close

Key Takeaways

How does a receiver know that no more values will come? In Waiting for Goroutines with Done Channels, the receiver always knew the number in advance: three goroutines started, so main received exactly three times. That works when the count is fixed before the work begins, and it fails when it is not.

A goroutine that reads lines from a stream, pages from a search, or jobs from a queue usually does not know how many values it will produce until it has produced the last one. The receiver knows even less. It needs a message that says "there are no more values", and that message must be different from every ordinary value the channel can carry.

Every Go channel can carry that message. The sections below cover how a sender marks a channel as finished, how a receiver detects it, and why only the sender should do it.

Closing a Channel

The built-in function close marks a channel as finished. The Go specification says that close(ch) "records that no more values will be sent on the channel". It does not destroy the channel or discard anything in it.

A channel on which close has been called is a closed channel. Closing is a signal from the sender to every receiver, and three facts follow from it:

  • Values already in the buffer stay there, and receivers still get them in order.
  • Once the buffer is empty, every receive completes at once and returns the zero value of the element type.
  • A send on a closed channel causes a run-time panic, and so does closing it a second time.

The second fact is what lets a receiver detect the end. A receive from an open, empty channel waits, but a receive from a closed, empty channel never waits.

The Comma-Ok Receive

A zero value alone is not enough, because a sender may also send a zero value on purpose. An empty string or a 0 can be real data. A receive can therefore return a second result, a boolean, which is written as v, ok := <-ch. This form is called the comma-ok receive.

The value of ok is true when v was sent by a send statement, and false when v is a zero value produced because the channel is closed and empty. The program below sends two values into a buffered channel, closes it, and receives three times.

Go
Go

. . . .

Output:

"page a" true "" true "" false len after close: 0 cap: 3

Both values sent before close are received normally, including the empty string, and each one arrives with ok set to true. The third receive finds the channel closed and empty. It returns at once with the zero value "" and ok set to false. Without the second result, the second and third receives would look identical.

Ranging over a Channel

A loop that receives until the channel is closed is common enough that Go has a statement for it. A for range loop over a channel receives one value per iteration and ends when the channel is closed and empty. It is the same as a loop of comma-ok receives that breaks when ok is false.

The program below uses the course's fetch function. A producer goroutine fetches three pages, sends each one, and closes the channel after the last send. The receiver in main does not know how many pages to expect.

Go
Go

. . . .

Output:

page from https://example.com/a page from https://example.com/b page from https://example.com/c channel closed after 3 pages

The loop in main prints each page as it arrives and stops by itself after the third. The receiver never states a count. If the URL list grew to fifty, only the caller of fetchAll would change. The parameter pages chan<- string is send-only, as described in Channel Direction in Function Signatures, and close is allowed on it because closing is a sender's operation.

A range loop ends after close
A range loop ends after close

A Range Loop with No Close

If the sender never closes the channel, the range loop has no way to end. After the last real value, the loop waits for another one. The program below is the same pattern with the call to close left out.

Go
Go

. . . .

It prints both pages and then stops with:

fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main()

Here the runtime detects the deadlock, because the sending goroutine has ended and main is the only goroutine left. In a larger program, other goroutines are usually still running, so nothing is reported. The loop simply waits forever, and the code after it never runs.

Only the Sender Closes

The documentation of close says that it "should be executed only by the sender, never the receiver". The reason is the panic rule. A send on a closed channel panics, and the receiver cannot know whether the sender is about to send again.

The program below shows the mistake. The receiving code needs only two pages, so it closes the channel to tell the producer to stop.

Go
Go

. . . .

It prints the two pages and then the panic. The goroutine number differs between runs.

page from https://example.com/a page from https://example.com/b panic: send on closed channel goroutine 20 [running]: main.produce(...)

The producer's third send finds the channel closed, and the panic ends the whole program, main included. Closing carries information in one direction only, from the sender to the receivers. A request from a receiver to stop the sender needs a separate channel, one that the sender receives from.

If pages had been declared as <-chan string inside the receiving code, the call to close would not even compile. The compiler reports cannot close receive-only channel for it, which is one more reason to give every parameter a direction.

When to Close

Closing is not a cleanup step. A channel that is no longer referenced is freed by the garbage collector whether it was closed or not, so there is no need to close a channel only to release it.

Close a channel when its receivers need to know that the values have ended, and that usually means a receiver is ranging over it. When the receiver already knows the count, as with the done channels in Waiting for Goroutines with Done Channels, a close adds nothing.

Receive formOpen, value availableOpen, emptyClosed, values leftClosed, empty
v := <-chValueWaitsValueZero value at once
v, ok := <-chValue, trueWaitsValue, trueZero value, false
for v := range chNext iterationWaitsNext iterationLoop ends

Key Takeaways

  • close(ch) records that no more values will be sent; values already buffered are still delivered.
  • After a channel is closed and empty, every receive returns the zero value at once.
  • The comma-ok receive v, ok := <-ch sets ok to false only for that zero value, so real zero values stay distinguishable.
  • for v := range ch receives until the channel is closed and empty; without a close, it waits forever.
  • Only the sender closes, because a send on a closed channel panics.

Closing a channel is how a sender tells its receivers that the values have ended. Receivers detect it with the comma-ok receive or a range loop, and they never close the channel themselves, because only the sender knows when the last value has been sent.

Reading Progress

0%


Vote for new content

On This Page

Closing a Channel

The Comma-Ok Receive

Ranging over a Channel

A Range Loop with No Close

Only the Sender Closes

When to Close

Key Takeaways