Hello everyone, I am the fisherman of “Go School”. Today I’m going to talk to you about nil channels and how they can be used properly.

The original link: mp.weixin.qq.com/s/IIruvES-U…

Sometimes forgetting to use nil channels in Go is a common mistake. In this section, let’s take a look at what a nil channel is and why we use it.

First, suppose we have the following code snippet in a coroutine:

// Initialize channel to nil
var ch chan int
<-ch
Copy the code

So how does this code execute? Ch is of type int. The zero value of channel is nil, because ch is defined but not initialized, so ch is currently nil. In Go, receiving a message from a nil channel is a legal operation. The coroutine does not cause panic; But the coroutine will be blocked forever.

If sending a message to a nil channel follows the same principle, the coroutine will block permanently:

var ch chan int
ch <- 0
Copy the code

So why allow messages to be sent or received from nil channels in Go? Let’s discuss the purpose of this design with a concrete example.

Merge (ch1, ch2 chan int) chan int. Merge (ch1, ch2 chan int) chan int. Merge (ch1, ch2 chan int) chan int. Merge (ch1, ch2 chan int) chan int. Merge (ch1, ch2 chan int)

Ok, now let’s implement that.

Implementation version one: for loop version

func merge(ch1, ch2 <-chan int) <-chan int {
    ch := make(chan int.1)
    
    go func(a) {
        for v := range ch1 {
            ch <- v
 	}
        
 	for v := range ch2 {
            ch <- v
 	}
        
 	close(ch)
     }()
    
     return ch
}
Copy the code

In the merge function, we start a coroutine that receives messages from both channels through the for loop and then sends them to the CH channel.

Version one problem

The main problem in this implementation version is that we receive information from CH1 and then from CH2. That is, you can only receive messages from CH2 if CH1 is turned off, otherwise you’re stuck in CH1. This obviously doesn’t fit our usage scenario, if CH1 is never turned off, then messages in CH2 will never be received. What we want is to receive messages from both channels.

Implementation version 2: Select version

Since we can’t use the for loop, we use the SELECT statement to improve the concurrency, as follows:

func merge(ch1, ch2 <-chan int) <-chan int {
    ch := make(chan int.1)
 	
    go func(a) {
        for {
            select {
            case v := <-ch1:
                ch <- v
            case v := <-ch2:
                ch <- v
            }
 	}
        
 	close(ch)
    }()
    
    return ch
}
Copy the code

The SELECT statement allows the coroutine to listen for multiple channel operations at the same time. Because we encapsulated select in the for loop, we repeatedly received information from CH1 or CH2.

Version 2 problem

One problem is that the close(ch) statement is never executed. If you loop through a channel through range, the range ends when the channel is closed. However, if you loop in for/ SELECT mode, the for loop does not end even if CH1 or CH2 is turned off. Worse, if at some point ch1 or CH2 is turned off, the coroutine used to receive from the merge channel CH will always receive 0 (because ch is of type int, int’s default zero is 0), as follows:

received: 0
received: 0
received: 0
received: 0
received: 0.Copy the code

Why is that? Because receiving information from a closed channel is not blocked.

ch1 := make(chan int)
close(ch1)
fmt.Print(<-ch1, <-ch1)
Copy the code

We might expect the above code to either panic or block, but it executes normally and prints 0, 0. In fact, what we receive from the closed channel is a zero value representing a closed event rather than an actual message. We can check whether we received a message or a zero value for the close event by using the following code:

ch1 := make(chan int)
close(ch1)
v, open := <-ch1
fmt.Print(v, open)
Copy the code

The channel output has another value that indicates whether the channel is closed: the open variable. We can use this value to determine whether the channel is closed or not:

0.false
Copy the code

Also, if the channel is closed, the zero value of the channel type is assigned to the first variable.

So, in implementation version 2, if CH1 is turned off, the code also does not execute as expected. For example, if the select statement had chosen V := <-ch1, we would have blocked here and kept sending zero values to the merged channel.

Implementation version three: State variable version

Since in version 2, if a channel is closed, it continues to receive zeros of the corresponding type and send them to channel CH, which is responsible for merging data. Then, we can use a state variable to identify whether the channel is closed and not send to merge data channel CH when it is closed. As follows:

func merge(ch1, ch2 <-chan int) <-chan int {
    ch := make(chan int.1)
    ch1Closed := false
    ch2Closed := false
    
     go func(a) {
         for {
            select {
            case v, open := <-ch1:
                if! open { ch1Closed =true
                }else {
                    ch <- v
                }
                
            case v, open := <-ch2:
                if! open { ch2Closed =true
                }else {
                    ch <- v
                }
            }
            
            if ch1Closed && ch2Closed { 
                close(ch)
                return}}} ()return ch
}
Copy the code

In this version of the implementation, we defined two Boolean variables ch1Closed and ch2Closed, representing the closed state of channels CH1 and CH2, respectively. Once we receive a message from a channel, we check to see if the channel is closed. If so, the corresponding state variables (ch1Closed and ch2Closed) are set to true. When both channels are closed, we close the merge result channel CH and terminate the coroutine.

Version 3 problem

In addition to complicating the code, there is a major problem: when either ch1 or CH2 is closed, the for loop will continue because the coroutine will still receive a zero from the closed channel, even if it does not send a zero to the channel responsible for merging messages, CH. For example, if CH1 is a closed channel, select will always select the first case statement when there is no new message from CH2, execute continuously to receive zero from CH1, then break, and then execute for. At the same time, it is a waste of CPU because it is constantly receiving null values in a loop.

Implementation version 4:

Now, the nil channel comes out. Using the property that all reads and writes to nil are permanently blocked, we combine the multi-way listening feature of SELECT to achieve this. The code is as follows:

func merge(ch1, ch2 <-chan int) <-chan int {
    ch := make(chan int.1)
 	
    go func(a) {
 	forch1 ! =nil|| ch2 ! =nil { 
            select {
            case v, open := <-ch1:
                if! open { ch1 =nil 
                }else {
                    ch <- v
                }
 			
            case v, open := <-ch2:
                if! open { ch2 =nil
 		}else {
                    ch <- v
                }
            }
 	}
 
        close(ch)
        
     }()
 
     return ch
}
Copy the code

In this implementation, we loop as long as one channel (CH1 or CH2) is open. Let’s say ch1 is closed, let’s set ch1 to nil. Therefore, in the next loop, the SELECT statement either waits for a new message from CH2 or receives a closed signal when CH2 is closed. Because ch1 is nil, it’s no longer selected by the SELECT statement. Finally, when both CH1 and CH2 are turned off, we close ch, the channel responsible for merging information. Here is a flowchart for the entire implementation:

In this version implementation, the program executes as we expect. At the same time, the problem of CPU waste in version 3 was solved.

Anyway, we’re taking advantage of the fact that sending or receiving information to a nil channel will block. This feature can be useful in certain scenarios. In our example, we removed the corresponding channel (CH1 or CH2) from the SELECT listener by setting it to nil.

If you enjoyed this article, check out Go School for more error-prone highlights.