Channel in Go language

Channel is a special data type in Go language. The channel itself is concurrent and safe. It can transfer data between multiple goroutine s. Channel is the perfect realization of Go language programming concept: "Do not communicate by sharing memory; instead, share memory by communicating". It is often encountered in concurrent programming. Let's introduce how to use the channel.

Channel sending and receiving

Channels include two-way channels and one-way channels. Here, two-way channels only support sending and receiving channels, while one-way channels can only send or receive channels.

Bidirectional channel

Declare and initialize a channel using the make function:

ch1 := make(chan string, 3)
  • chan is a keyword representing the channel type
  • string indicates the element type of the channel type
  • 3 indicates that the capacity of the channel is 3, and up to 3 element values can be cached.

A channel is equivalent to a first in first out (FIFO) queue. Use the operator < - to send and receive element values:

ch1 <- "1"  //Send data "1" to channel ch1

Receive element value:

elem1 := <- ch1 // Element value in receive channel

The first received element is the element value stored in the channel first, that is, first in first out:

package main

import "fmt"

func main() {
	str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, len(str1))

	for _, str := range str1 {
		ch1 <- str
	}
	
	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}
}

Execution results:

hello
world
!

Unidirectional channel

Unidirectional channels include send only channels and receive only channels:

var WriteChan = make(chan<- interface{}, 1) // Only channels that cannot be received can be sent
var ReadChan = make(<-chan interface{}, 1) // Only channels that cannot be sent can be received

This feature of one-way channel can be used to constrain the input type or output type of the function. For example, the following example restricts that element values can only be received from the channel:

package main

import (
	"fmt"
)

func OnlyReadChan(num int) <-chan int {
	ch := make(chan int, 1)
	ch <- num
	close(ch)
	return ch
}

func main() {

	Chan1 := OnlyReadChan(6)
	num := <- Chan1
	fmt.Println(num)
}

Execution results:

6

Channel blocking

Channel operations are concurrent and safe. At the same time, only one of any sending operations for the same channel will be executed. Other sending operations for the channel may not be executed until the element value is completely copied into the channel. The same is true for receive operations. In addition, for the same element value in the channel, the send operation and the receive operation are mutually exclusive.

Sending and receiving operations are atomic operations, that is, only a part of the sending operation will never be copied, either it has not been copied or it has been copied. After the copy of the element value is prepared for the receiving operation, the original value in the channel will be deleted, and there will never be any residue in the channel. During sending and receiving operations, the code will always be blocked there. After the operation is completed, the following code will continue to be executed. The sending and receiving operations of the channel are very fast, so under what circumstances will there be long-term blocking? Several situations are described below.

Blocking of buffer channel

A buffer channel is a channel with a capacity greater than 0, that is, a channel that can cache data.

1. Send blocking

If the buffer channel is full, it will be blocked if goroutine continues to send data to the channel. Take the following example:

package main

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	ch1 <- 2
}

Execution results:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
...........

If the channel can receive data (some elements are received), the channel will notify the goroutine waiting for the sending operation to execute the sending operation again.

2. Receive blocking

Similarly, if the channel is empty, it will be blocked if the receiving operation continues.

package main

func main() {
	ch1 := make(chan int, 1)
	<- ch1
}

Execution results:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
...........

Unbuffered channel

A non buffered channel is a channel with a capacity of 0 and cannot cache data.

The data transmission of non buffered channels is synchronous, and the transmission operation or reception operation will be blocked after execution. The transmission will not continue until the corresponding reception operation or transmission operation is executed. It can be seen that the buffer channel uses asynchronous data transmission.

package main

import (
    "fmt"
)

func main() {
    str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, 0)

    go func() {
        for _, str := range str1 {
            ch1 <- str
        }
    }()

	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}

}

Execution results:

hello
world
!  

In the above code, the three goroutine s write data to the channel three times, and they must receive it three times, otherwise it will be blocked.

Blocking also occurs when sending and receiving a channel with a value of nil:

var ch1 chan int
ch1 <- 1 // block
<-ch1 // block

Channel closed

You can use the close() method to close the channel. After the channel is closed, you can no longer send the channel, but receive it.

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)
    
    ele := <-ch1
	fmt.Println(ele)  
    
	ch1 <- 2
}

Execution results:

1
panic: send on closed channel

goroutine 1 [running]:
.....

If there are elements inside when the channel is closed, the returned channel closing flag is still true when receiving:

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)

	ele1, statu1 := <-ch1
	fmt.Println(ele1, statu1)
	ele2, statu2 := <-ch1
	fmt.Println(ele2, statu2)
}

Execution results:

1 true
0 false

Due to this characteristic of the channel, the sender can close the channel. The previous example can be written as follows:

package main

import (
    "fmt"
)

func main() {
    str1 := []string{"hello","world", "!"}
	ch1 := make(chan string, 0)

    go func() {
        for _, str := range str1 {
            ch1 <- str
        }
        close(ch1)
    }()

	for i := 0; i < len(str1); i++ {
		elem := <- ch1
		fmt.Println(elem)
	}

}

In addition, closed channels cannot be closed again:

package main

// import "fmt"

func main() {
	ch1 := make(chan int, 1)
	ch1 <- 1
	close(ch1)
	close(ch1)

}

Execution results:

panic: close of closed channel

select statement and channel

The select statement is usually used with channels and is designed for channels. When the select statement is executed, generally only one case expression or default statement will be run.

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	num := 2
	
	select {
		case data := <-ch1:
			fmt.Println("Read data: ", data)
		case ch1 <- num:
			fmt.Println("Write data: ", num)	
		default:
			fmt.Println("No candidate case is selected!")
	}
}

Execution results:

Write data:  2

It should be noted that if there is no default branch and case expressions do not meet the conditions, the select statement will be blocked until at least one case expression meets the conditions.

If multiple branches meet the conditions at the same time, one branch will be selected randomly for execution

When a for statement is used in conjunction with a select statement, the break statement in the branch can only end the execution of the current select statement without exiting the for loop. The following code will never exit the loop:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	for {
		select {
		case ch1 <- 6:
			fmt.Println("Write data: 6")
		case data := <-ch1:
			fmt.Println(data)
			break
		}
	}
}

The solution is to use goto statements and tags.

Method 1:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 1)
	num := 6
	for {
		select {
		case ch1 <- num:
			fmt.Println("Write data: ", num)
		case data := <-ch1:
			fmt.Println("Read data: ", data)
			goto loop
		}
	}	
	loop:
	fmt.Println(ch1)
}

Execution results:

Write data:  6
Read data:  6
0xc00000e0e0

Method 2:

package main

import "fmt"

func main() {
	ch1 := make(chan int, 1)
	num := 6
	loop:
	for {
		select {
		case ch1 <- num:
			fmt.Println("Write data: ", num)
		case data := <-ch1:
			fmt.Println("Read data: ", data)
			break loop
		}
	}
	fmt.Println(ch1)
}

Execution results:

Write data:  6
Read data:  6
0xc0000e4000

summary

This paper mainly introduces the basic operations of the channel: initialization, sending, receiving and closing. Pay attention to what conditions will cause channel blocking. Select statements are usually used with channels. This paper introduces the selection rules of branches and how to exit the loop when for statements are used with select statements.

Channel is an important implementation basis of Go language concurrent programming, and it is still necessary to master it.

--THE END--

Tags: Go Programming

Posted on Sat, 18 Sep 2021 00:15:32 -0400 by jwbworks