Gold, an experimental Go local docs server, Go docs generation tool, and code reader. NEW!

-- show type implemention relations --

-- show code statistics --

-- smooth code view experiences --

-- and more... --

How to Gracefully Close Channels

Several days ago, I wrote an article which explains the channel rules in Go. That article got many votes on reddit and HN, but there are also some criticisms on Go channel design details. I collected some criticisms on the following designs and rules of Go channels: no easy and universal ways to check whether or not a channel is closed without modifying the status of the channel. closing a closed channel will panic, so it is dangerous to close a channel if the closers don't know whether or not the channel is closed. sending values to a closed channel will panic, so it is dangerous to send values to a channel if the senders don't know whether or not the channel is closed. I collected some criticisms on the following designs and rules of Go channels:

The criticisms look reasonable (in fact not). Yes, there is really not a built-in function to check whether or not a channel has been closed.

package main import "fmt" type T int func IsClosed(ch There is indeed a simple method to check whether or not a channel is closed if you can make sure no values were (and will be) ever sent to the channel. The method has been shown in the last article . Here, for a better coherence, the method is listed in the following example again. As above mentioned, this is not a universal way to check whether a channel is closed.

In fact, even if there is a simple built-in closed function to check whether or not a channel has been closed, its usefulness would be very limited, just like the built-in len function for checking the current number of values stored in the value buffer of a channel. The reason is the status of the checked channel may have changed just after a call to such functions returns, so that the returned value has already not been able to reflect the latest status of the just checked channel. Although it is okay to stop sending values to a channel ch if the call closed(ch) returns true , it is not safe to close the channel or continue sending values to the channel if the call closed(ch) returns false .

The Channel Closing Principle

One general principle of using Go channels is don't close a channel from the receiver side and don't close a channel if the channel has multiple concurrent senders. In other words, we should only close a channel in a sender goroutine if the sender is the only sender of the channel.

(Below, we will call the above principle as channel closing principle.)

Surely, this is not a universal principle to close channels. The universal principle is don't close (or send values to) closed channels. If we can guarantee that no goroutines will close and send values to a non-closed non-nil channel any more, then a goroutine can close the channel safely. However, making such guarantees by a receiver or by one of many senders of a channel usually needs much effort, and often makes code complicated. On the contrary, it is much easy to hold the channel closing principle mentioned above.

Solutions Which Close Channels Rudely

T ). func SafeClose(ch chan T) (justClosed bool) { defer func() { if recover() != nil { // The return result can be altered // in a defer function call. justClosed = false } }() // assume ch != nil here. close(ch) // panic if ch is closed return true // <=> justClosed = true; return } If you would close a channel from the receiver side or in one of the multiple senders of the channel anyway, then you can use the recover mechanism to prevent the possible panic from crashing your program. Here is an example (assume the channel element type is). This solution obviously breaks the channel closing principle. The same idea can be used for sending values to a potential closed channel. func SafeSend(ch chan T, value T) (closed bool) { defer func() { if recover() != nil { closed = true } }() ch closed = false; return } The same idea can be used for sending values to a potential closed channel. Not only does the rude solution break the channel closing principle, and data races might happen in the process.

Solutions Which Close Channels Politely

sync.Once to close channels: 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) }) } Many people prefer usingto close channels: Surely, we can also use sync.Mutex to avoid closing a 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() defer mc.mutex.Unlock() if !mc.closed { close(mc.C) mc.closed = true } } func (mc *MyChannel) IsClosed() bool { mc.mutex.Lock() defer mc.mutex.Unlock() return mc.closed } Surely, we can also useto avoid closing a channel multiple times: These ways may be polite, but they may not avoid data races. Currently, Go specification doesn't guarantee that there are no data races happening when a channel close and a channel send operations are executed concurrently. If a SafeClose function is called concurrently with a channel send operation to the same channel, data races might happen (though such data races generally don't much harm).

Solutions Which Close Channels Gracefully

One drawback of the above SafeSend function is that its calls can't be used as send operations which follow the case keyword in select blocks. The other drawback of the above SafeSend and SafeClose functions is that many people, including me, would think the above solutions by using panic / recover and sync package are not graceful. Following, some pure-channel solutions without using panic / recover and sync package will be introduced, for all kinds of situations.

(In the following examples, sync.WaitGroup is used to make the examples complete. It may be not always essential to use it in real practice.)

1. M receivers, one sender, the sender says "no more sends" by closing the data channel

package main import ( "time" "math/rand" "sync" "log" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumReceivers = 100 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) // the sender go func() { for { if value := rand.Intn(Max); value == 0 { // The only sender can close the // channel at any time safely. close(dataCh) return } else { dataCh This is the simplest situation, just let the sender close the data channel when it doesn't want to send more.

2. One receiver, N senders, the only receiver says "please stop sending more" by closing an additional signal channel

channel closing principle. But we can let the receiver close an additional signal channel to notify senders to stop sending values. package main import ( "time" "math/rand" "sync" "log" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(1) // ... dataCh := make(chan int) stopCh := make(chan struct{}) // stopCh is an additional signal channel. // Its sender is the receiver of channel // dataCh, and its receivers are the // senders of channel dataCh. // senders for i := 0; i < NumSenders; i++ { go func() { for { // The try-receive operation is to try // to exit the goroutine as early as // possible. For this specified example, // it is not essential. select { case This is a situation a little more complicated than the above one. We can't let the receiver close the data channel to stop data transferring, for doing this will break the. But we can let the receiver close an additional signal channel to notify senders to stop sending values. As mentioned in the comments, for the additional signal channel, its sender is the receiver of the data channel. The additional signal channel is closed by its only sender, which holds the channel closing principle. In this example, the channel dataCh is never closed. Yes, channels don't have to be closed. A channel will be eventually garbage collected if no goroutines reference it any more, whether it is closed or not. So the gracefulness of closing a channel here is not to close the channel.

3. M receivers, N senders, any one of them says "let's end the game" by notifying a moderator to close an additional signal channel

channel closing principle. However, we can introduce a moderator role to close the additional signal channel. One trick in the following example is how to use a try-send operation to notify the 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 Max = 100000 const NumReceivers = 10 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) stopCh := make(chan struct{}) // stopCh is an additional signal channel. // Its sender is the moderator goroutine shown // below, and its receivers 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, and its receiver is // the moderator goroutine shown below. // It must be a buffered channel. var stoppedBy string // moderator go func() { stoppedBy = This is a the most complicated situation. We can't let any of the receivers and the senders close the data channel. And we can't let any of the receivers close an additional signal channel to notify all senders and receivers to exit the game. Doing either will break the. However, we can introduce a moderator role to close the additional signal channel. One trick in the following example is how to use a try-send operation to notify the moderator to close the additional signal channel. In this example, the channel closing principle is still held. Please note that the buffer size (capacity) of channel toStop is one. This is to avoid the first notification is missed when it is sent before the moderator goroutine gets ready to receive notification from toStop . We can also set the capacity of the toStop channel as the sum number of senders and receivers, then we don't need a try-send select block to notify the moderator. ... toStop := make(chan string, NumReceivers + NumSenders) ... value := rand.Intn(Max) if value == 0 { toStop We can also set the capacity of thechannel as the sum number of senders and receivers, then we don't need a try-sendblock to notify the moderator.

4. A variant of the "M receivers, one sender" situation: the close request is made by a third-party goroutine

package main import ( "time" "math/rand" "sync" "log" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 100000 const NumReceivers = 100 const NumThirdParties = 15 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) closing := make(chan struct{}) // signal channel closed := make(chan struct{}) // The stop function can be called // multiple times safely. stop := func() { select { case closing Sometimes, it is needed that the close signal must be made by a third-party goroutine. For such cases, we can use an extra signal chanel to notify the sender to close the data channel. For example, The idea used in the stop function is learned from a comment made by Roger Peppe.

5. A variant of the "N sender" situation: the data channel must be closed to tell receivers that data sending is over

channel closing principle, we avoid closing the data channels. However, sometimes, it is required that the data channels must be closed in the end to let receivers know data sending is over. For such cases, we can translate a N-sender situation to a one-sender situation by using a middle channel. The middle channel has only one sender, so that we can close it instead of closing the original data channel. package main import ( "time" "math/rand" "sync" "log" "strconv" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const Max = 1000000 const NumReceivers = 10 const NumSenders = 1000 const NumThirdParties = 15 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int) // will be closed middleCh := make(chan int) // will never be closed closing := make(chan string) // signal channel closed := make(chan struct{}) var stoppedBy string // The stop function can be called // multiple times safely. stop := func(by string) { select { case closing In the solutions for the above N-sender situations, to hold the, we avoid closing the data channels. However, sometimes, it is required that the data channels must be closed in the end to let receivers know data sending is over. For such cases, we can translate a N-sender situation to a one-sender situation by using a middle channel. The middle channel has only one sender, so that we can close it instead of closing the original data channel.

More situations?

There should be more situation variants, but the above shown ones are the most common and basic ones. By using channels (and other concurrent programming techniques) cleverly, I believe a solution holding the channel closing principle for each situation variant can always be found.

Conclusion

There are no situations which will force you to break the channel closing principle. If you encounter such a situation, please rethink your design and rewrite you code.

Programming with Go channels is like making art.