SoFunction
Updated on 2025-03-05

The correct way to use the channel in actual combat

1. Why use channel

The author is also a player who has switched from Java to Go. It has been difficult for him to get rid of data structures such as thread pools, reentrant locks, AQS and other data structures and their underlying thinking patterns. Recently, the author has also begun to gradually review past internships and experiments, and slowly understand some of the experiences of golang concurrency.

When golang solves concurrent race problems,The first solution to consider is to use channel. Many people may like to use mutex locks, because mutex lock only has two operations: Lock and Unlock, which are similar to ReentrantLock in Java. However, during my practice, I found:

Mutex locks can only block and cannot communicate between processes. If communication is required between different processes, a mechanism similar to a semaphore is needed. At the same time, it is best that this mechanism can achieve process control. For example, control the order of execution of different tasks, let the tasks wait for unfinished tasks, and interrupt a certain rotation state.

How to implement these functions? Channel is an elegant answer given by Go. (Of course it does not mean that channel can completely replace locks, and locks can make code and logic simpler)

2. Basic operations

2.1 channel

The channel can be regarded as a FIFO queue, and the queues are in and out of the queue are all atomic operations. The types of elements inside the queue can be selected freely. The following are common operations for channel

//initializationss := make(chan struct{})
sb := make(chan bool)
var s chan bool
si = make(chan int)
// Writesi <- 1
sb <- true
ss <- struct{}
//read<-sb
i := <-si
(i+1)//2
// The channel that has been used can be closedclose(si)

2.2 channel cache

Generally speaking, channels are available with cache and without cache.

Channel read and write without cache are blocked, Once a write operation occurs in a channel, unless another goroutine uses a read operation to remove the element from the channel,Otherwise, the current goroutine will be blocked. On the contrary, if a channel without cache is read by a goroutine, the current goroutine will be blocked until another goroutine initiates a write to the channel.

The result of the following unit test is that the compiler reports an error, prompting a deadlock.

func TestChannel0(t *) {
	c := make(chan int)
	c <- 1
}

fatal error: all goroutines are asleep - deadlock!

If it is to run correctly, it should be modified to

func TestChannel0(t *) {
	c := make(chan int)
	go func(c chan int) { <-c }(c)
	c <- 1
}

The feature of channel with channel cache is that when there is cache space, data can be written and returned directly, and when there is data in the cache, it can be read directly.. If the cache space is full and not read at the same time, the write will block. Similarly, if there is no data in the cache space, read in will also block until data is written.

//It will be executed successfullyfunc TestChannel1(t *) {
	c := make(chan int,1)
	go func(c chan int) { c &lt;- 1 }(c)
	&lt;-c
}

//It will not be deadlocked because the cache space is not filledfunc TestChannel2(t *) {
	c := make(chan int,1)
	c&lt;-1
}

//It will be deadlocked because the cache space will continue to be written after it is filled.func TestChannel3(t *) {
	c := make(chan int,1)
	c&lt;-1
	c&lt;-1
}

//It will be deadlocked because it keeps reading blocking and has not been written.func TestChannel4(t *) {
	c := make(chan int,1)
	&lt;-c
}

2.3 Read-only write channel

Some channels can be defined as only for writing, or for sending.

Below are specific examples

func sender(c chan&lt;- bool){
	c &lt;- true
	//<- c // This sentence will cause an error}
func receiver(c &lt;-chan bool){
	//c <- true// This sentence will cause an error	&lt;- c
}
func normal(){
	senderChan := make(chan&lt;- bool)
	receiverChan := make(&lt;-chan bool)
}

2.4 select

select allows goroutine to listen to multiple channel operations simultaneously. When a case clause can be run, the logic below the case will be executed and the select statement ends. If the default statement is defined and the execution in each case is blocked and cannot be completed, the program will enter the default logic.

It is worth noting that if there are multiple cases that can be satisfied, the final case statement executed is uncertain (different from the switch statement that determines whether it is satisfied from top to bottom).

Here is an example to illustrate

func writeTrue(c chan bool) {
	c &lt;- false
}
// The output is chan 1, because chan 1 has readable datafunc TestSelect0(t *) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	writeTrue(chan1)
	select {
	case &lt;-chan1:
		("chan 1")
	case &lt;-chan2:
		("chan 2")
	default:
		("default")
	}
}
// The output is default, because neither chan1 nor chan2 can read datafunc TestSelect1(t *) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	select {
	case &lt;-chan1:
		("chan 1")
	case &lt;-chan2:
		("chan 2")
	default:
		("default")
	}
}
// The output is chan 1 or chan 2, because both chan 1 and chan 2 have readable datafunc TestSelect2(t *) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	writeTrue(chan1)
	writeTrue(chan2)
	select {
	case &lt;-chan1:
		("chan 1")
	case &lt;-chan2:
		("chan 2")
	default:
		("default")
	}
}

2.5 for range

The for range loop of the channel can read data from the channel in turn. You don’t know how many elements there are before reading the data. If there are no elements in the channel, it will block and wait until the channel is closed and exit the loop. If the logic of closing the channel is not in the code, or the break statement is inserted, a deadlock will occur.

func testLoopChan() {
	c := make(chan int)
	go func() {
		c &lt;- 1
		c &lt;- 2
		c &lt;- 3
		( * 2)
		close(c)
	}()
	for x := range c {
		("test:%+v\n", x)
	}
}

//resulttest:1
test:2
test:3
Finish

It should be noted here that the object polled by the for range can be considered to have been taken out of the channel. Let's take two examples to illustrate:

func testLoopChan2() {
	c := make(chan int)
	go func() {
		c &lt;- 1
		c &lt;- 2
		c &lt;- 3
	}()
	for x := range c {
		("test:%+v\n", x)
		break
	}
	&lt;-c
	&lt;-c
}
//Output1

func testLoopChan3() {
	c := make(chan int)
	go func() {
		c &lt;- 1
		c &lt;- 2
		c &lt;- 3
	}()
	for x := range c {
		("test:%+v\n", x)
		break
	}
	&lt;-c
	&lt;-c
	&lt;-c
}
//The output deadlock is because the channel has been empty, and the last <- operation will cause blockage

III. Use

3.1 State machine rotation

A core usage of channel is process control. For state machine rotation scenarios, channel can be easily solved (classic take-off printing of ABC).

func main(){
    chanA :=make(chan struct{},1)
    chanB :=make(chan struct{},1)
    chanC :=make(chan struct{},1)
    
    chanA<- struct{}{}
    
    go printA(chanA,chanB)
    go printB(chanB,chanC)
    go printC(chanC,chanA)
}

func printA(chanA chan struct{}, chanB chan struct{}) {
    for {
        <-chanA
        println("A")
        chanB<- struct{}{}
    }
}

func printB(chanB chan struct{}, chanC chan struct{}) {
    for {
        <-chanB
        println("B")
        chanC<- struct{}{}
    }
}

func printC(chanC chan struct{}, chanA chan struct{}) {
    for {
        <-chanC
        println("C")
        chanA<- struct{}{}
    }
}

3.2 Process Exit

This is a small skill I got in the raft experiment, using a channel to indicate whether I need to exit. Listen to the channel in select, once written, it can enter the exit logic

exit := make (chan bool)
//...
for {
	select {
		case <-exit:
			("exit code")
			return
		default:
			("normal code")
			//...
	}
}

3.3 Timeout control

This is also the skill I got in the raft experiment. If a task returns, it can be written in the channel corresponding to the task and read it by select. Use a case to time at the same time. If the time is exceeded and it still does not complete, the timeout logic will be entered.

func control(){
	taskAChan := make (chan bool)
	TaskA(taskAChan)
	select {
		case &lt;-taskAChan:
			("taskA success")
		case &lt;- &lt;-(5 * ):
			("timeover")
	}
}

func TaskA(taskAChan chan bool){
	//TaskA's main code	//...
	// Write channel only after completing TaskA	taskAChan &lt;- true
}

3.4 Goroutine pool with concurrency limit

During my internship, I encountered a requirement that I needed to initiate ftp requests to the target server concurrently, but the number of connections that can be initiated at the same time is limited, and the buffer channel needs to control it. The channel is a bit similar to a semaphore, and read and write will cause changes in the cache space. The role of cache here is similar to a semaphore (write and read corresponding PV operation). The channel is written when performing a task, and the channel is read when completing the task. If the cache space is exhausted, new write requests will block until a task completes the cache space is released.

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem &lt;- 1 // Wait for release;    process(r)
    // It may take a long process;    &lt;-sem // Complete, release another process.}

func Serve(queue chan *Request) {
    for {
        req := &lt;-queue
        go handle(req) // No need to wait for the handle to complete.    }
}

3.5 Overflow cache

In a high concurrency environment, in order to avoid request loss, you can choose to cache requests that are not processed in the future. This is also a function that can be achieved using select. If a buffer channel is full, cache it in the default logic.

func put(c message){
	select {
		case putChannel <- c:
			("put success")
		default:
			("buffer data")
			buffer(c)
	}
}

3.6 Random probability distribution

select {
        case b := <-backendMsgChan:
        if sampleRate > 0 && rand.Int31n(100) > sampleRate {
            continue
        } 
}

4. Pits and experiences

4.1 panic

The following situations can lead to panic

  • Close the nil channel
  • Close and write the closed channel (read will read out the zero value)

You can use the ok value to check whether the channel is empty or closed

queue := make(chan int, 1)

value, ok := <-queue
if !ok {
    ("queue is closed or nil")
	queue = nil
}

4.2 If the closed channel is used, it will return in advance.

The channel closing will cause the range to return

4.3 Write the reset channel

If a structure's channel member has a chance to be reset, its write must be considered for failure.

In the following example, the write jumps to the default logic

type chanTest struct {
	c chan bool
}

func TestResetChannel(t *) {
	cc := chanTest{c: make(chan bool)}
	go ()
	select {
	case  <- true:
		(" in")
	default:
		("default")

	}
}

func (c *chanTest) resetChan() {
	 = make(chan bool)
}

Summarize

This is the article about the correct way to use channels in the actual combat of golang. For more information about correct use of golang channel, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!