A few days ago, I wrote an article explaining the use of channel in Golang. In Reddit and HN, that article received a lot of approval, but I also received the following criticisms about the go channel design and specifications:
- In the absence of changes to the channel state, there is no simple and common way to check if the channel has been closed
- Closing the already closed channel can cause panic, so it is dangerous to close the channel if the closer (the person shutting down) doesn't know if the channel is closed.
- Sending a value to a channel that has been closed causes panic, so it is dangerous to send a value to the channel if the sender (sender) does not know if the channel is closed.
Those criticisms seem to make sense (not really). Yes, there is no built-in function to check if a channel has been closed. If you can be sure that no value will be sent to the channel, then you do need an easy way to check if the channel is closed:
package main
import "fmt"
type T int
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}
As mentioned above, there is no suitable way to check whether the channel has been closed. However, even if there is a simpleclosed(chan T) boolfunction to check whether the channel has been closed, its usefulness is limited, as is the built-inlenfunction to check the number of elements in the buffer channel. The reason is that the status of the channel that has been checked is likely to be modified after a similar method has been called, so the returned value cannot reflect the current state of the channel just checked.
Although itclosed(ch)is possible to stop sending a value to the channel when the call returns, it istrueclosed(ch)falsenot safe to close the channel or continue to send a value to the channel if the call Returns (panic).
The Channel Closing Principle
When using Go channel, one of the principles is not to close the channel from the receiving end, nor to close the channel with multiple concurrent senders. In other words, if sender (sender) is only sender or the last active sender of the channel, then you should close the channel at sender's Goroutine, notifying receiver (s) (receiver) There is no value to read. Maintaining this principle will ensure that it never occurs. Send a value to an already closed channel or close a channel that has been closed.
(Below, we will call the above principle the channel closing principle
Solutions to break channel closing principle
If you shut down the channel from the receiving end (receiver side) for some reason or one of the multiple senders, you should use the column Golang Panic/recover Cases function to safely send values into the channel (assuming that the channel element type is T)
func SafeSend(ch chan T, value T) (closed bool) {
defer func() {
if recover() != nil {
// the return result can be altered
// in a defer function call
closed = true
}
}()
ch <- value // panic if ch is closed
return false // <=> closed = false; return
}
If the channel ischnot closed, then the performance of this function will bech <- valueclose. When the channel is closed, theSafeSendfunction is only called once per sender Goroutine, so the program does not have much performance penalty.
The same idea can be used to close the channel from multiple goroutine:
func SafeClose(ch chan T) (justClosed bool) {
defer func() {
if recover() != nil {
justClosed = false
}
}()
// assume ch != nil here.
close(ch) // panic if ch is closed
return true
}
Many people like to use itsync.Onceto close the channel:
type MyChannel struct {
C chan T
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func(){
close(mc.C)
})
}
Of course, we can also usesync.Mutexto avoid closing the channel multiple times:
type MyChannel struct {
C chan T
closed bool
mutex sync.Mutex
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.mutex.Lock()
if !mc.closed {
close(mc.C)
mc.closed = true
}
mc.mutex.Unlock()
}
func (mc *MyChannel) IsClosed() bool {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.closed
}
We should understand why go does not support built-inSafeSendandSafeClosefunctions, because it is not recommended to close the channel from the receiving end or multiple concurrent senders. Golang even prohibits the closing of the channel that receives only (RECEIVE-ONLY).
Keep Channel closing principle elegant solution
One drawback of the aboveSafeSendfunction is that itcasecannot be invoked as a send operation after the keyword of the SELECT statement (Translator Note: similar tocase SafeSend(ch, t):). Another drawback is that many people, including myself, feel that the scheme above ispanicnot elegant by using/recoverandsyncpackages. For a variety of scenarios, the following describes the usepanicrecoverof/andsyncpackages, the purely use of channel solutions.
(In the following example, itsync.WaitGroupis only used to make the example complete.) Its use is not always useful in practice.)
- M receivers, a sender,sender by closing the data channel and saying "no more sending"
This is the simplest scenario, just let sender close the channel when sender doesn't want to send it again:
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumReceivers = 100
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
// ...
dataCh := make(chan int, 100)
// the sender
go func() {
for {
if value := rand.Intn(MaxRandomNumber); value == 0 {
// the only sender can close the channel safely.
close(dataCh)
return
} else {
dataCh <- value
}
}
}()
// receivers
for i := 0; i < NumReceivers; i++ {
go func() {
defer wgReceivers.Done()
// receive values until dataCh is closed and
// the value buffer queue of dataCh is empty.
for value := range dataCh {
log.Println(value)
}
}()
}
wgReceivers.Wait()
}
- a receiver,n sender,receiver by closing an extra signal channel saying "Please stop sending"
This scenario is a bit more complicated than the previous one. We cannot let receiver close the data channel because doing so will break the channel closing principle . But we can have receiver close an additional signal channel to notify sender to stop sending the value:
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(1)
// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh is an additional signal channel.
// Its sender is the receiver of channel dataCh.
// Its reveivers are the senders of channel dataCh.
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
value := rand.Intn(MaxRandomNumber)
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}()
}
// the receiver
go func() {
defer wgReceivers.Done()
for value := range dataCh {
if value == MaxRandomNumber-1 {
// the receiver of the dataCh channel is
// also the sender of the stopCh cahnnel.
// It is safe to close the stop channel here.
close(stopCh)
return
}
log.Println(value)
}
}()
// ...
wgReceivers.Wait()
}
As the note says, for the extra signal channel, its sender is receiver of the data channel. This additional signal channel was closed by its only sender, followed by the channel closing principle.
- M receiver,n sender, either of them, by notifying a moderator (arbitrator) to close the additional signal channel, "Let's End the game."
This is the most complicated scenario. We cannot allow arbitrary receivers and senders to close the data channel, nor can any one receivers notify all senders and receivers to exit the game by closing an additional signal channel. Doing so would break the channel closing principle. However, we can introduce a moderator to close an additional signal channel. A tip for this example is how to inform moderator to close the additional signal channel:
package main
import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
// ...
const MaxRandomNumber = 100000
const NumReceivers = 10
const NumSenders = 1000
wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)
// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh is an additional signal channel.
// Its sender is the moderator goroutine shown below.
// Its reveivers are all senders and receivers of dataCh.
toStop := make(chan string, 1)
// the channel toStop is used to notify the moderator
// to close the additional signal channel (stopCh).
// Its senders are any senders and receivers of dataCh.
// Its reveiver is the moderator goroutine shown below.
var stoppedBy string
// moderator
go func() {
stoppedBy = <- toStop // part of the trick used to notify the moderator
// to close the additional signal channel.
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(MaxRandomNumber)
if value == 0 {
// here, a trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "sender#" + id:
default:
}
return
}
// the first select here is to try to exit the
// goroutine as early as possible.
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()
for {
// same as senders, the first select here is to
// try to exit the goroutine as early as possible.
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case value := <-dataCh:
if value == MaxRandomNumber-1 {
// the same trick is used to notify the moderator
// to close the additional signal channel.
select {
case toStop <- "receiver#" + id:
default:
}
return
}
log.Println(value)
}
}
}(strconv.Itoa(i))
}
// ...
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}
In this case, the channel closing principleis still adhered to.
Note thattoStopthe buffer size of the channel is 1. This is to avoid the loss of the first notification that was sent before Mederator Goroutine was ready.
- More scenes?
Many of the scenario variants are based on the above three types. For example, a variant based on the most complex scenario might require receivers to read all the remaining values in the buffer channel. This should be easy to handle, and all this article will not be mentioned.
Although the above three scenarios do not cover all of the go channel usage scenarios, they are the most basic, and most of the scenarios in practice can be categorized into those three types.
Conclusion
There is no scene here that requires you to break the channel closing principle. If you encounter this scenario, consider your design and rewrite your code.
Programming with GO is like creating art.