SoFunction
Updated on 2025-03-05

Detailed explanation of Channel of the important components that Golang cannot avoid during concurrency

In the previous article, I introduced the important keywords for Golang to achieve concurrencygo, through this we can easily and quickly start the Goroutinue coroutine. There must be communication requirements between coroutines, and Golang's core design idea is:Memory should be shared not by sharing memory. There is a slight difference from other programming languages ​​that pass data through shared memory, and Channel is the one that implements this solution.

Channel is a queue type that provides data types for concurrent function communication that can receive and send specific types of values, and meets the FIFO (first in, first out) principle. FIFO is reflected in both data types and operations:

  • Elements of Channel type are first-in, first-out, and elements sent to Channel first will be received.
  • Goroutinue that sends data to the channel first will be executed first
  • Goroutinue that receives data from the channel first will be executed first

Channel usage

grammar

channel is a data type in Golang, and the related syntax is also very simple.

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType 

channel is a channel type keyword

<- operator is used for sending and receiving data in channel, and is used to indicate the direction of channel data flow when declared.

  • chan defaults to two-way delivery, that is, channel can receive and send data
  • chan<- Only channel that can send data
  • <-chan only channel that can accept data

ElementType represents element type, such as int, string...

initialization

The channel data type is a reference type, similar to map and slice, so the initialization of the channel requires the use of the built-in function make():

make(ChannelType, Capacity)

ch := make(chan int) 
var ch = make(chan int) 
ch := make(chan int, 10) 
ch := make(<-chan int) 
ch := make(chan<- int, 10)
  • ChannelType is the type introduced earlier
  • Capacity stands for buffer capacity. When omitted, it is the default 0, indicating an unbuffered Channel.

If the channel is not initialized without using the make() function, the sending and receiving communication operations cannot be performed, and it will cause blockage, which will cause Goroutinue leakage. Example:

func main() {
	defer func() {
		("goroutines: ", ())
	}()
	var ch chan int
	go func() {
		<-ch
	}()
	()
}

The code execution result is:

goroutines:  2

As you can see, until the program exits, the number of Goroutinues is still 2. The reason is that the channel is not initialized correctly using make(), and the channel variable is actually nil, which in turn causes memory leakage.

Receiving and sending data

The reception and transmission of data in the channel are operated through the operator <-:

// Receive datach &lt;- Expression
ch &lt;- 111
ch &lt;- struct{}{}
// Send data&lt;- ch
v := &lt;- ch
f(&lt;-ch)

In addition to the operator <-, we can also usefor rangeContinuously receive data from the channel:

for e := range ch {
    // e reads the element values ​​in ch one by one}

There is no big difference between continuous reception and <-:

  • If ch is nil channel, it will block
  • If ch does not send the element, it will block.

for will continue to read until the channel is closed. After closing, for will read all remaining elements and end. So what happens when sending and receiving data on a closed channel?

Channel's closure

After using the channel, use the built-in function close() to close the channel. Turning off the channel means recording that the channel cannot be sent any elements anymore, rather than destroying the channel. This means that the closed Channel can continue to receive the value.

  • If sending data to a closed channel, panic will be triggered
  • Close nil channel will trigger panic
  • Close the closed channel will cause panic
  • If you read the channel value that has been closed, you can receive all the values ​​sent before closing; after receiving the value before closing, the zero value of type and a false will be returned, and will not block and panic

You can write a simple code to test the above situations.

Channel classification

As mentioned earlier, when making a channel, the second parameter represents the buffer size. If there is no second parameter, it is the default unbuffered channel. Specific usage:

  • Buffer Channel, make(chan T, cap), cap is a value greater than 0.
  • Unbuffered Channel, make(chan T), make(chan T, 0)

Unbuffered channel

Unbuffered channels are also called synchronous channels, and communications will only succeed if both the sender and the receiver are ready.

Synchronization operation example:

func ChannelSync() {
// Initialize the datach := make(chan int)
wg := {}
    // Send interval    (1)
    go func() {
        defer ()
        for i := 0; i &lt; 5; i++ {
            ch &lt;- i
            println("Send ", i, ".\tNow:", ().Format("15:04:05.999999999"))
            // Interval time            (1 * )
        }
        close(ch)
    }()
    // Interval reception    (1)
    go func() {
        defer ()
        for v := range ch {
            println("Received ", v, ".\tNow:", ().Format("15:04:05.999999999"))
            // Interval time, note that the interval time is different from send            (3 * )
        }
    }()
    ()
}

Execution results:

Send  0 .       Now: 17:54:27.772773
Received  0 .   Now: 17:54:27.772795
Received  1 .   Now: 17:54:30.773878
Send  1 .       Now: 17:54:30.773959
Received  2 .   Now: 17:54:33.775132
Send  2 .       Now: 17:54:33.775208
Received  3 .   Now: 17:54:36.775816
Send  3 .       Now: 17:54:36.775902
Received  4 .   Now: 17:54:39.776408
Send  4 .       Now: 17:54:39.776456

In the code, a synchronous channel is used, and two goroutines are used to complete the sending and receiving. The time intervals for each sending and receiving are different. We print the sent and received values ​​and times respectively. You can see the execution results: the sending and receiving time is the same; the interval is based on the long one, and it can be seen that the sending and receiving operations are synchronous operations. Therefore, synchronous channel is suitable for synchronous signals between gotoutines

Buffer Channel

Buffered Channel is also called asynchronous Channel, and the receiver and sender can succeed without waiting for both parties to be ready. There will be a buffer space with cap capacity in the buffer Channel. When using buffered Channel communication, the receive and send operations are operating the Channel's Buffer, which is a typical queue operation:

  • When receiving, the element is received from the buffer, as long as the buffer is not empty, it will not block. On the contrary, the buffer is empty, which will block and the goroutine hangs
  • When sending, send elements to the buffer, and as long as the buffer is not full, it will not block. On the contrary, if the buffer is full, it will block and the goroutine will hang

Operation example:

func main() {
	// Initialize the data	ch := make(chan int, 5)
	wg := {}
	// Send interval	(1)
	go func() {
		defer ()
		for i := 0; i &lt; 5; i++ {
			ch &lt;- i
			println("Send ", i, ".\tNow:", ().Format("15:04:05.999999999"))
			// Interval time			(1 * )
		}
	}()
	// Interval reception	(1)
	go func() {
		defer ()
		for v := range ch {
			println("Received ", v, ".\tNow:", ().Format("15:04:05.999999999"))
			// Interval time, note that the interval time is different from send			(3 * )
		}
	}()
	()
}

Execution results:

Send  0 .       Now: 17:59:32.990698
Received  0 .   Now: 17:59:32.99071
Send  1 .       Now: 17:59:33.992127
Send  2 .       Now: 17:59:34.992832
Received  1 .   Now: 17:59:35.991488
Send  3 .       Now: 17:59:35.993155
Send  4 .       Now: 17:59:36.993445
Received  2 .   Now: 17:59:38.991663
Received  3 .   Now: 17:59:41.99184
Received  4 .   Now: 17:59:44.992214

In the code, it is consistent with the synchronous channel, but a buffer channel with a capacity of 5 is used, and two goroutines are used to complete the sending and receiving. The time intervals for each sending and receiving are different. We print the sent and received values ​​and times respectively. You can see the execution results: the sending and receiving time is different; the sending and receiving operations will not block, and it can be seen that the sending and receiving operations are asynchronous operations. Therefore, buffered channels are very suitable for data communication between goroutines

Channel principle

Source code

In the runtime/ in the source code package, you can see the channel implementation source code:

type hchan struct {
    qcount   uint           // The number of elements is obtained by len()    dataqsiz uint           // The length of the buffer queue, that is, the capacity is obtained through cap()    buf       // Buffer queue pointer, unbuffered queue is nil    elemsize uint16         // Element size    closed   uint32         // Close sign    elemtype \*\_type // Element type    sendx    uint   // Send element index    recvx    uint   // Receive element index    recvq    waitq  // Receive Goroutinue queue    sendq    waitq  // Send Goroutinue queue        // lock protects all fields in hchan, as well as several
        // fields in sudogs blocked on this channel.
        //
        // Do not change another G's status while holding this lock
        // (in particular, do not ready a G), as this can deadlock
        // with stack shrinking.
    lock mutex // Lock}

buf can be understood as a ring array used to cache elements in the Channel. Why use ring arrays instead of ordinary arrays? Because ordinary arrays are more suitable for the specified space, when elements are popped up, all ordinary arrays need to be moved forward, and using ring arrays + subscript indexes can achieve efficient reading and writing of data without moving elements.

Sendx and recvx will return to the first position when the subscript exceeds the array capacity, so there are two fields that need to record the current read and written subscript positions.

recvq and sendq are used to record the goroutine queue waiting for reception and transmission. When the received or sent goroutine based on a certain channel cannot be understood and executed, that is, when blocking is required, it will be recorded in the wait queue of the Channel. When the channel can complete the corresponding reception or send operation, the goroutine is awakened from the waiting queue to perform operations.

The waiting queue is actually a two-way linked list structure

life cycle

Create a policy

  • Direct allocation of memory without buffering
  • The buffered one does not contain pointers, assigns contiguous addresses to hchan and underlying arrays
  • A buffered channel and contains element pointers, which will assign addresses to hchan and underlying arrays.

Send policy

  • Convert to function when sending operation is compiled
  • Blocking: block=true; non-blocking: block=false
  • Sending data to the channel is divided into two parts: inspection and data transmission, data transmission:
    • If the channel's read wait queue exists for the recipient goroutinue
      • Send data directly to the first waiting goroutinue to wake up the received goroutinue
    • If the channel read waiting queue does not exist for the receiver goroutinue
      • If the loop array buf is not full, the data is sent to the end of the loop array buf
      • If the loop array buf is full, the sending process will be blocked, the current goroutinue will be added to the write waiting queue, and the waiting waiting to wake up will be suspended

Receive Policy

  • The receive operation is converted to a function
  • Blocking: block=true; non-blocking: block=false
  • Receive data from channel:
    • If the write waiting queue of channel exists as a sender goroutinue:
      • If it is an unbuffered channel, copy the data directly from the first sender goroutinue to the receiving variable, and wake up the sent goroutinue
      • If there is a buffer channel (full), copy the queue element of the loop array buf to the receiving variable, copy the data of the first sender goroutinue to the buf loop array, and wake up the sent goroutinue
    • If the write wait for channel does not exist for sender goroutinue
      • If the loop array buf is not empty, copy the header element of the loop array buf to the receiving variable
      • If the loop array buf is empty, the reception process will be blocked, and the current goroutine will be added to the read waiting queue and the waiting waiting to wake up will be suspended.

closure

Calling a function

A simple explanation of some basic usage and principles of Channel is given. You can write more concurrent code and read the source code to deepen your understanding of Channel.

The above is the detailed explanation of Channel, an important component that Golang cannot avoid, for concurrency. For more information about Golang Channel, please pay attention to my other related articles!