• There is no simple universal way to check if a channel is closed without changing its state.

  • Closing a closed channel can cause panic, so it’s dangerous to close a channel if closer doesn’t know if it’s closed.

  • Sending a value to a channel that is already closed can cause panic, so it is dangerous for the sender to send a value to a channel without knowing if it is already closed.

How to gracefully close a channel?

When using A Go Channel, a good rule of thumb is not to close a channel from the receiver, or from multiple concurrent transmitters. In other words, if the sender is only the sender or the last active sender for a channel, you should close the channel in the sender’s goroutine to inform the receiver(s) that there are no more values to read. Maintaining this principle ensures that it never happens to send a value to a closed channel or to close a closed channel. (We will call the above principles the Channel Closing principle.)

Keep the elegant solution of Channel Closing Principle

The channel closing principle requires that we can only close a channel at the sending end, which can be divided into three categories:

  1. M receivers, a sender.

  2. M receivers, a sender.

  3. M receivers,n sender



01 M receivers, a sender

M receivers, a sender, the sender says “no more sending” by closing the Data channel. This is the simplest scenario, just telling the sender to close the data to close the channel when it doesn’t want to send any more:

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)
    // sender
    go func() {
        for {
            ifvalue := rand.Intn(MaxRandomNumber); Value == 0 {// Only sender can safely close channel.close (dataCh)return
            } else {
                dataCh <- value
            }
        }
    }()
    // receivers
    for i := 0; i < NumReceivers; i++ {
      go func() {defer wgReceivers.Done() // Will wait and receive the message until dataCh closes the buffer queue and dataCh is emptyfor value := range dataCh {
              log.Println(value)
          }
      }()
    }
    wgReceivers.Wait()
}Copy the code


02One receiver,n Senders

One receiver, N sender, the scenario where the receiver says “Please stop sending” by shutting down an extra signal channel is a little more complicated than the last one. We can’t have a receiver close a data channel, because doing so would break the channel Closing principle. But we can tell the Receiver to close an extra signal channel to tell the sender to stop sending values:

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, Channel. stopCh := make(chan struct{}) // sendersfor 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 {
            ifValue == MaxRandomNumber-1 {// Receiver dataCh channel is also the sender of stopCh channel // here is safe to close stopCh channel close(stopCh)return} log.println (value)}}() // Wait until synchronization endsCopy the code


03 M receivers,n sender

M receivers, N Sender, any one of them saying ‘let’s finish the game’ by notifying a moderator to close the additional signal channel this is the most complex scenario. We cannot make any of the receivers and Senders close the data channel, nor can any of the receivers tell all of the Senders and Receivers to quit the game by closing an additional signal channel. Doing so breaks the channel Closing principle. However, we can bring in a moderator to close an additional signal channel. One trick of this example is how to inform moderators to close additional signal channels:

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 following moderator goroutine. // Its reveivers are all senders and receivers of dataCh. toStop := make(chan string, 1) // Channel toStop is used to inform the moderator // to close the additional signal channel (stopCh). // Its senders are the receivers of any senders and dataCh here. // Its Reveiver is the following Moderator goroutine. Var stoppedBy string // Moderator gofuncClose (stopCh)}() {stoppedBy = < -tostop // Used to alert the moderator to close additional signal channels. close(stopCh)}() // sendersfor i := 0; i < NumSenders; i++ {
        go func(id string) {
            for {
                value := rand.Intn(MaxRandomNumber)
                ifValue == 0 {// Here a select is used to inform the moderator // to close additional signals channel.select {case toStop <- "sender#" + id:
                    default:
                    }
                    return} // Select as early as possible to exit the current goroutine 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{// As with senders, the first select is used to exit the current goroutine.select {case <- stopCh:
                    return
                default:
                }
                select {
                case <- stopCh:
                    return
                case value := <-dataCh:
                    ifValue == maxrandomNumber-1 {// Use the same trick to alert the moderator // to close additional signals channel.select {case toStop <- "receiver#" + id:
                        default:
                        }
                        return
                    }
                    log.Println(value)
                }
            }
        }(strconv.Itoa(i))
    }
    wgReceivers.Wait()
    log.Println("stopped by", stoppedBy)
}Copy the code



Break the channel Closing principle

Is there a built-in function that checks if a channel is closed? If you can be sure that no value will be sent to a channel, then you really need a simple way to check if a channel is closed:

package main

import "fmt"

type T intfunc 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
}Copy the code

As mentioned above, there is no applicable way to check if a channel is closed. However, even if there is a simple closed(chan T) bool function to check whether a channel is closed, it is of limited use, as is the built-in Len function to check the number of elements in a buffered channel. The reason is that the state of a checked channel may change after a similar method is called, so that the value returned does not reflect the current state of the checked channel.

Although it is ok to stop sending values to a channel if a call to closed(ch) returns true, it is not safe to close the channel or continue sending values to the channel if the call to closed(ch) returns false.

The Channel Closing Principle

When using A Go Channel, a good rule of thumb is not to close a channel from the receiver, or from multiple concurrent transmitters. In other words, if the sender is only the sender or the last active sender for a channel, then you should notify the receiver(s)(receivers) that there are no values to read by closing the channel with the sender’s Goroutine. Maintaining this principle ensures that it never happens to send a value to a closed channel or to close a closed channel. (We will call the above principles the Channel Closing principle.)

Solution for breaking channel Closing Principle

If you close a channel from the receiver side or from one of multiple senders for some reason, Then you should Use the function listed in Golang panic/ Recover Use Cases to safely send values to channels (assuming channel element type T).

func SafeSend(ch chan T, value T) (closed bool) {
    defer func() {
        ifrecover() ! = nil { // thereturn result can be altered
             // in a defer function call
            closed = true
        }
    }()
    ch <- value
    // panic if ch is closed
    return false 
    // <=> closed = false; return
}Copy the code

If channel ch is not closed, the performance of this function is close to that of ch < -value. When a channel is closed, the SafeSend function is called only once in each Sender Goroutine, so there is no significant performance penalty. The same idea can be used to close channels from multiple Goroutines:

func SafeClose(ch chan T) (justClosed bool) {
    defer func() {
        ifrecover() ! = nil { justClosed =false} }() // assume ch ! = nil here. close(ch) // panicif ch is closed
    return true
}Copy the code


Many people like to close a channel with sync.once:

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)    })
}Copy the code

Of course, 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()    
    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
}Copy the code

It is important to understand why Go does not support the built-in SafeSend and SafeClose functions because closing channels from the receiver or multiple concurrent transmitters is not recommended. Golang even forbids closing receive-only channels.


Thanks for reading!


If you like this article, welcome to “ISevena”.