Channel idioms
suggest changeChannels exhibit the following properties:
- send to a nil channel blocks forever
- receive from a nil channel blocks forever
- send to a closed channel panics
- receive from a closed channel returns the zero value immediately
Send to a nil channel blocks forever
package main
func main() {
var ch chan bool
ch <- true // deadlocks because ch is nil
}
Uninitialized value of a channel is nil
so the above program blocks forever.
There's no valid use case for that so it's always a bug. Don't do it.
Receive from a nil channel blocks forever
package main
import "fmt"
func main() {
var ch chan bool
fmt.Printf("Value received from ch is: %v\n", <-ch) // deadlock because c is nil
}
Similarly, receive from nil
channel blocks forever and it's always a bug. Don't do it.
Send to a closed channel panics
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan int, 100)
go func() {
ch <- 1
time.Sleep(time.Second)
close(ch)
ch <- 1
}()
for i := range ch {
fmt.Printf("i: %d\n", i)
}
}
Output:
i: 1
panic: send on closed channel
goroutine 5 [running]:
main.main.func1(0x452000, 0xc99)
/tmp/sandbox307976305/main.go:14 +0xa0
created by main.main
/tmp/sandbox307976305/main.go:10 +0x60
You should architecture your programs so that one sender controls the lifetime of a channel.
This rule emphasizes that: if there's only one channel sender then there's no problem making sure that you never write to a closed channel.
If you have multiple senders then it becomes hard: if one sender closes a channel, how other senders are supposed to not crash?
Instead of trying to find solution to the above problem, re-architect your code so that there's only one sender that controls the lifetime of a channel.
Receive from a closed channel returns the zero value immediately
package main
import "fmt"
func main() {
// show
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for i := 0; i < 3; i++ {
fmt.Printf("%d ", <-ch) // -> 1 2 0
}
// show end
}
This is not ideal: we've read a value from a channel that was never sent.
It's easy to remedy:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
// show
for {
v, ok := <-ch
if !ok {
break
}
fmt.Printf("%d ", v) // -> 1 2
}
// show end
}
And even better and more idiomatically:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
// show
for v := range ch {
fmt.Printf("%d ", v) // -> 1 2
}
// show end
}
Closing channel to signal a goroutine has finished
Sometimes you need to wait until a goroutine has finished.
The fact that a receive from a closed channel returns immediately but otherwise blocks can be used to coordinate between goroutines by a shared "done" channel.
One goroutine closes the channel at the end.
The other goroutine can either wait indefinitely until that happens with a receive: chDone <-
In more complex scenarios it can receive from multiple channels with select
statement. For example, to limit a waiting time:
select {
case <- chDone:
// goroutine has finished
case <- time.After(time.Second *5):
// goroutine didn't finish but we don't want to wait
// more than 5 seconds
}
To check if channel is closed (i.e. goroutine has finished) without waiting:
package main
import "fmt"
// show
func checkState(ch chan struct{}) {
select {
case <-ch:
fmt.Printf("channel is closed\n")
default:
fmt.Printf("channel is not closed\n")
}
}
// show end
func main() {
// show
ch := make(chan struct{})
checkState(ch)
close(ch)
checkState(ch)
// show end
}
This technique is used in context.Done()
channel.