Most new Go programmers quickly grasp the idea of a channel as a queue of values and are comfortable with the notion that channel operations may block when full or empty.
This post explores four of the less common properties of channels:
- A send to a nil channel blocks forever
- A receive from a nil channel blocks forever
- A send to a closed channel panics
- A receive from a closed channel returns the zero value immediately
A send to a nil channel blocks forever
The first case which is a little surprising to newcomers is a send on a nil
channel blocks forever.
This example program will deadlock on line 5 because the zero value for an uninitalised channel is nil
.
package main func main() { var c chan string c <- "let's get started" // deadlock }
http://play.golang.org/p/1i4SjNSDWS
A receive from a nil channel blocks forever
Similarly receiving from a nil
channel blocks the receiver forever.
package main import "fmt" func main() { var c chan string fmt.Println(<-c) // deadlock }
http://play.golang.org/p/tjwSfLi7x0
So why does this happen ? Here is one possible explanation
- The size of a channel’s buffer is not part of its type declaration, so it must be part of the channel’s value.
- If the channel is not initalised then its buffer size will be zero.
- If the size of the channel’s buffer is zero, then the channel is unbuffered.
- If the channel is unbuffered, then a send will block until another goroutine is ready to receive.
- If the channel is
nil
then the sender and receiver have no reference to each other; they are both blocked waiting on independent channels and will never unblock.
A send to a closed channel panics
The following program will likely panic as the first goroutine to reach 10 will close the channel before its siblings have time to finish sending their values.
package main import "fmt" func main() { var c = make(chan int, 100) for i := 0; i < 10; i++ { go func() { for j := 0; j < 10; j++ { c <- j } close(c) }() } for i := range c { fmt.Println(i) } }
http://play.golang.org/p/hxUVqy7Qj-
So why isn’t there a version of close()
that lets you check if a channel is closed ?
if !isClosed(c) { // c isn't closed, send the value c <- v }
But this function would have an inherent race. Someone may close the channel after we checked isClosed(c)
but before the code gets to c <- v
.
Solutions for dealing with this fan in problem are discussed in the 2nd article linked at the bottom of this post.
A receive from a closed channel returns the zero value immediately
The final case is the inverse of the previous. Once a channel is closed, and all values drained from its buffer, the channel will always return zero values immediately.
package main import "fmt" func main() { c := make(chan int, 3) c <- 1 c <- 2 c <- 3 close(c) for i := 0; i < 4; i++ { fmt.Printf("%d ", <-c) // prints 1 2 3 0 } }
http://play.golang.org/p/ajtVMsu8EO
The correct solution to this problem is to use a for range
loop.
for v := range c { // do something with v } for v, ok := <- c; ok ; v, ok = <- c { // do something with v }
These two statements are equivalent in function, and demonstrate what for range
is doing under the hood.
Very nice. Thanks for putting this together. I actually hadn’t realized that receiving from a closed channel would immediately return zero until reading Sameer Ajmani’s post on the Golang blog, Go Concurrency Patterns: Pipelines and cancellation. Frankly I found it a little surprising. I thought it was use of the bool second return value that allowed a receive operation to cancel itself on closure. The realization was almost enough to make me do something just like this to demonstrate subtleties of channels. So thanks again.
Anyway. There’s a small syntax error in your ‘manual range’. A missing assignment.
for v, ok := <- c; ok ; v, ok = <- c {
// do something with v
}
Cheers
Thanks for catching that mistake in the example, I’ve fixed it now.