Table of Contents
- Channel Hints
Channel Hints
-
A send to a nil channel blocks forever.
This example program will deadlock because the zero value for an uninitalised channel is nil.
package main
func main() {
var c chan string
c <- "let's get started" // deadlock
}
-
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
}
So why does this happen ?
-
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)
}
}
-
A receive from a closed channel returns the zero value immediately.
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
}
}
The correct solution to this problem is to use a for range loop.
package main
import "fmt"
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
close(c)
for v := range c {
fmt.Printf("%d ", v) // prints 1 2 3
}
}
-
A closed channel never blocks.
Once a channel has been closed, you cannot send a value on this channel, but you can still receive from the channel.
package main
import "fmt"
func main() {
ch := make(chan bool, 2)
ch <- true
ch <- true
close(ch)
for i := 0; i < cap(ch)+1; i++ {
v, ok := <-ch
fmt.Println(v, ok)
}
}
Running the program shows we retrieve the first two values we sent on the channel, then on our third attempt the channel gives us the values of false and false. The first false is the zero value for that channel’s type, which is false, as the channel is of type chan bool. The second indicates the open state of the channel, which is now false, indicating the channel is closed.
Let’s start with this example that combined with select.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
finish := make(chan bool)
var done sync.WaitGroup
done.Add(1)
go func() {
select {
case <-time.After(1 * time.Hour):
case <-finish:
}
done.Done()
}()
t := time.Now()
finish <- true // send the close signal
done.Wait() // wait for the goroutine to stop
fmt.Printf("Waited %v for goroutine to stop\n", time.Since(t))
}
But there are a few problems with this program.
-
The first is the finish channel is not buffered, so the send to finish may block if the receiver forgot to add finish to their select statement. You could solve that problem by wrapping the send in a select block to make it non blocking, or making the finish channel buffered.
-
However what if you had many goroutines listening on the finish channel, you would need to track this and remember to send the correct number of times to the finish channel. This might get tricky if you aren’t in control of creating these goroutines; they may be being created in another part of your program, perhaps in response to incoming requests over the network.
A nice solution to this problem is to leverage the property that a closed channel is always ready to receive. Using this property we can rewrite the program, now including 100 goroutines, without having to keep track of the number of goroutines spawned, or correctly size the finish channel.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
const n = 100
finish := make(chan bool)
var done sync.WaitGroup
for i := 0; i < n; i++ {
done.Add(1)
go func() {
select {
case <-time.After(1 * time.Hour):
case <-finish:
}
done.Done()
}()
}
t := time.Now()
close(finish) // closing finish makes it ready to receive
done.Wait() // wait for all goroutines to stop
fmt.Printf("Waited %v for %d goroutines to stop\n", time.Since(t), n)
}
As soon as the finish channel is closed, it becomes ready to receive. As all the goroutines are waiting to receive either from their time.After channel, or finish, the select statement is now complete and the goroutines exits after calling done.Done() to deincrement the WaitGroup counter. This powerful idiom allows you to use a channel to send a signal to an unknown number of goroutines, without having to know anything about them, or worrying about deadlock.
-
A nil channel always blocks.
A nil channel; a channel value that has not been initalised, or has been set to nil will always block.
Let’s start with this example that wait for channels a and b to close.
package main
import (
"fmt"
"time"
)
func WaitFor(a, b chan bool) {
for a != nil || b != nil {
select {
case <-a:
a = nil
case <-b:
b = nil
}
}
}
func main() {
a, b := make(chan bool), make(chan bool)
t0 := time.Now()
go func() {
close(a)
close(b)
}()
WaitFor(a, b)
fmt.Printf("waited %v for WaitFor\n", time.Since(t0))
}
In the WaitFor() we nil the reference to a or b once they have received a value. When a nil channel is part of a select statement, it is effectively ignored, so niling a removes it from selection, leaving only b which blocks until it is closed, exiting the loop without spinning.
-
Closing the nil channel cause runtime panic , and Closing a closed channel cause runtime panic too .
-
Do not close a channel from a receiver goroutine. Closing the channel from a receiver could make future sender goroutines to panic .
-
If a channel has multiple senders, do not close the channel from a sender goroutine. Closing the channel from a sender could make future sender goroutine to panic.
-
It is not required to close an unused channel. If no goroutine is left referencing the channel, it will be garbage collected. Note that it is only necessary to close a channel if the receiver is looking for a close.
-
Do not close a channel from one goroutine while writing to it from another goroutine.
Reference
- [1] Channel Axioms
- [2] Curious Channels
- [3] Golang Concurrency Tricks