Preface
Let’s take a look at an interview question:
What happens when reading and writing a closed chan? Why?
In the previous article learning Go coroutine, we know that the go keyword can be used to enable a goroutine for task processing, but if communication is required between multiple tasks, a channel is required.
1. Definition of Channel
To declare and initialize a channel, you can use the built-in function make in Go language, and specify the element type of the channel type. The following declares a channel of chan int type:
ch := make(chan int)
2. Channel operation
Send (write): The sending operation includes two steps: "Copy element value" and "Put the copy to the channel". That is: the entry of the channel is not the element value on the right side of the operator, but its copy.
ch := make(chan int) // write to channel ch <- x
Receive (read): The receiving operation includes three steps: "Copy the element value in the channel", "Put the copy to the receiver", and "Delete the original value".
ch := make(chan int) // read from channel x <- ch // another way to read x = <- ch
Close: Turning off the channel will generate a broadcast mechanism, and all goroutines that read messages to the channel will receive messages.
ch := make(chan int) close(ch)
Reading messages from a closed channel will never block, and will return an ok-iiom with false. You can use it to determine whether the channel is closed:
v, ok := <-ch
If ok is false, it means that the received v is the generated zero value, and this channel is closed or empty.
3. Characteristics of Channel sending and receiving operations
- A channel is equivalent to a first-in-first-out (FIFO) queue: that is, each element value in the channel is arranged strictly in the order of sending, and the element value of the channel sent first will be received first.
- For the same channel, the sending operations and the receiving operations are mutually exclusive: at the same time, multiple elements are sent to the same channel until the element value is completely copied into the channel, other sending operations for the channel can be performed. The same is true for reception.
- In the sending operation and receiving operation, the processing of element values is inseparable: we knew before that sending a value to the channel is to copy the value first and then move the copy to the inside of the channel. "Inseparable" means that the sending operation either has not copied the element value or has been copied, and there will never be only a part of it. The same is true for receiving. After preparing a copy of the element value, the original value in the channel will definitely be deleted, and there will never be any residual situations in the channel.
- The sending operation and the receiving operation will be blocked before it is fully completed: the sending operation includes two steps: "Copying element values" and "Posting a copy to the channel". Until these two steps are fully completed, the code that initiates the send operation will be blocked there, and the code after it will not have a chance to execute until the blocking is released.
4. Types of Channel
Channels are divided into channels without cache and channels with cache.
When using make to declare a channel type variable, in addition to specifying the element type of the channel, you can also specify the capacity of the channel, that is, how many element values the channel can cache at most. When the capacity is 0, the channel is a non-buffered channel, and when the capacity is greater than 0, the channel is a buffered channel.
ch := make(chan int) //Unbuffered channelch := make(chan int, 3) //The channel with buffering
Non-buffered channels and buffered channels have different data transfer methods:
- Non-buffered channel: Whether it is a sending operation or a receiving operation, it will be blocked at the beginning, and will not continue to pass until the paired operation also begins to be executed. That is: the data will be transmitted only when the sender and receive parties are connected. The data is copied directly from the sender to the receiver. The way data is passed by non-buffered channels is synchronous.
- Buffered channel: If the channel is full, all sending operations to it will be blocked until an element value is received in the channel. Conversely, if the channel is empty, all receiving operations on it will be blocked until a new element value appears in the channel. The element value will be copied from the sender to the buffer channel first, and then copied from the buffer channel to the receiver. The way the buffer channel passes data is asynchronous.
5. Channel source code learning
The main implementation of Channel is in src/runtime/, and the go version is go version go1.14.6 darwin/amd64. Here we mainly look at how chansend is implemented.
func chansend(c *hchan, ep , block bool, callerpc uintptr) bool { if c == nil { if !block { return false } gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) throw("unreachable") } if debugChan { print("chansend: chan=", c, "\n") } if raceenabled { racereadpc((), callerpc, funcPC(chansend)) } // Fast path: check for failed non-blocking operation without acquiring the lock. // // After observing that the channel is not closed, we observe that the channel is // not ready for sending. Each of these observations is a single word-sized read // (first and second or depending on kind of channel). // Because a closed channel cannot transition from 'ready for sending' to // 'not ready for sending', even if the channel is closed between the two observations, // they imply a moment between the two when the channel was both not yet closed // and not ready for sending. We behave as if we observed the channel at that moment, // and report that the send cannot proceed. // // It is okay if the reads are reordered here: if we observe that the channel is not // ready for sending and then observe that it is not closed, that implies that the // channel wasn't closed during the first observation. if !block && == 0 && (( == 0 && == nil) || ( > 0 && == )) { return false } var t0 int64 if blockprofilerate > 0 { t0 = cputicks() } lock(&) if != 0 { unlock(&) panic(plainError("send on closed channel")) } if sg := (); sg != nil { // Found a waiting receiver. We pass the value we want to send // directly to the receiver, bypassing the channel buffer (if any). send(c, sg, ep, func() { unlock(&) }, 3) return true } if < { // Space is available in the channel buffer. Enqueue the element to send. qp := chanbuf(c, ) if raceenabled { raceacquire(qp) racerelease(qp) } typedmemmove(, qp, ep) ++ if == { = 0 } ++ unlock(&) return true } if !block { unlock(&) return false } // Block on the channel. Some receiver will complete our operation for us. gp := getg() mysg := acquireSudog() = 0 if t0 != 0 { = -1 } // No stack splits between assigning elem and enqueuing mysg // on where copystack can find it. = ep = nil = gp = false = c = mysg = nil (mysg) gopark(chanparkcommit, (&), waitReasonChanSend, traceEvGoBlockSend, 2) // Ensure the value being sent is kept alive until the // receiver copies it out. The sudog has a pointer to the // stack object, but sudogs aren't considered as roots of the // stack tracer. KeepAlive(ep) // someone woke us up. if mysg != { throw("G waiting list is corrupted") } = nil = false if == nil { if == 0 { throw("chansend: spurious wakeup") } panic(plainError("send on closed channel")) } = nil if > 0 { blockevent(-t0, 2) } = nil releaseSudog(mysg) return true }
From the code you can see:
- There is a goroutine blocking on the channel recv queue. At this time, the cache queue is empty. The message is sent directly to the receiver goroutine, and only once replication is generated.
- When there is room left in the channel cache queue, put the data into the queue and wait for reception. After reception, two replications are generated in total.
- When the channel cache queue is full, the current goroutine is added to the send queue and blocked.
So, the initial interview question has the answer:
read:
Reading closed chan can keep reading the content, but the content read varies depending on whether there are elements before closing in the channel.
If there is an element in the buffer that has not been read before chan is closed, the value in chan will be read correctly, and the second bool value returned is true;
If before chan is closed, an element in the buffer has been read and there is no value in chan, it returns the zero value of the channel element, and the second bool value is false.
Write:
Writing a closed chan will panic.
Summarize
This is the end of this article about Channel sending and receiving operations in Go. For more information about Go Channel sending and receiving content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!