作为 Go 语言最有特色的数据类型,通道channel
完全可以与 协程goroutine
并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。
Don’t communicate by sharing memory; share memory by communicating. – by Rob Pike.
从 Go 语言创造者之一的这句话也能看出 Go 语言的重要编程理念,我们通常就是利用通道channel
在多个goroutine
中传递数据的。
通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。
类型字面量chan int
,其中的chan
是表示通道类型的关键字,而int
则说明了该通道类型的元素类型。chan string
代表了一个元素类型为string
的通道类型。
在初始化通道的时候,make
函数除了必须接收这样的类型字面量作为参数,还可以接收一个int
类型的参数。后者是可选的,用于表示该通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素值。由此,虽然这个参数是int
类型的,但是它是不能小于0
的。
ch1 := make(chan int, 3)
当容量为0
时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0
时,我们可以称为缓冲通道,也就是带有缓冲的通道。
通道是一个先进先出(FIFO)的队列。
接下来我们直接看Demo代码
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("element 1 received from channel ch1: %v\n", elem1)
elem2 := <-ch1
fmt.Printf("element 2 received from channel ch1: %v\n", elem2)
elem3, ret := <-ch1
if ret == true {
fmt.Printf("element 3 received from channel ch1: %v\n", elem3)
}
}
运行结果
element 1 received from channel ch1: 2
element 2 received from channel ch1: 1
element 3 received from channel ch1: 3
首先大家要知道,元素值从外界进入通道时会被复制,也就是说进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。
元素值从通道进入外界进行了两步操作:第一步是生成正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。
Go 程序执行后,同一个时刻只会执行对同一个通道的任意个发送操作中的某一个,直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。这也是channel
原生保证并发安全的底层逻辑。
为了保证通道中元素值的完整性,也是为了保证通道操作的唯一性。对于通道中的同一个元素值来说,只可能是某一个发送操作放入的,同时也只可能被某一个接收操作取出。
不会存在,A变量进行了拷贝了以后,还没移动进通道,就被B变量移动进去了,一定是一气呵成的。
跟进前面的特性,发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。
什么情况下会引起通道阻塞呢?
缓冲通道的情况:如果通道已满,所有发送操作都会被阻塞,直到通道中有元素值被接取走。然后通道会优先通知最早因此而等待的、那个发送操作所在的goroutine
,后者会再次执行发送操作。由于发送操作在这种情况下被阻塞后,它们所在的goroutine
会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。这时,通道会通知最早等待的那个接收操作所在的goroutine
,并使它再次执行接收操作。因此而等待的、所有接收操作所在的goroutine
,都会按照先后顺序被放入通道内部的接收等待队列。
非缓冲通道,简单一些。无论是发送还是接收,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。
根据上面2中缓冲方式能够看出,缓冲通道是在用异步的方式传递数据,非缓冲通道是用同步的方式传递数据。
对于值为nil
的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的goroutine
中的任何代码,都不再会被执行。
注意,由于通道类型是引用类型,所以它的零值就是nil
。当我们只声明该类型的变量但没有用make
函数对它进行初始化时,该变量的值就会是nil
,所以一定要初始化通道。
当我们把接收表达式的结果同时赋给两个变量时,第二个变量的类型就是一定bool
类型。它的值如果为false
就说明通道已经关闭,并且再没有元素值可取了。如果通道关闭时,里面还有元素值未被取出,那么接收表达式的第一个结果,仍会是通道中的某一个元素值,而第二个结果值一定会是true
。
关闭channel
后,无法向channel
再发送数据(引发panic
错误后导致接收立即返回零值);
关闭channel
后,可以继续从channel
接收数据;
下面我们继续看代码
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
close(ch1) // 如不执行该操作,那么elem4获取会报错
fmt.Printf("element 1 received from channel ch1: %v\n", elem1)
elem2 := <-ch1
fmt.Printf("element 2 received from channel ch1: %v\n", elem2)
elem3, ret := <-ch1
if ret == true {
fmt.Printf("element 3 received from channel ch1: %v\n", elem3)
}
elem4, ret4 := <-ch1
if ret4 == true {
fmt.Printf("element 4 received from channel ch1: %v\n", elem4)
}
}
为了保证程序的健壮性,在设计程序时,最好将channel
的读、写分别在goroutine
中进行,写完数据后,要关闭channel
。
示例代码如下:
var ch2 = make(chan int, 6)
func mm1() {
for i := 0; i < 10; i++ {
ch2 <- 8 * i
}
close(ch2)
}
func mm2() {
for {
for data := range ch2 {
fmt.Print(data, "\t")
}
}
}
func main() {
go mm1()
go mm2()
for {
runtime.GC()
}
}
可进行for
遍历,如下代码:
func main() {
ch3 := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch3 <- i
fmt.Printf("send %d\n", i)
time.Sleep(time.Second)
}
fmt.Println("ready close channel")
close(ch3)
}()
for i := range ch3 {
fmt.Printf("receive %d\n", i)
}
fmt.Println("quit for loop")
}
关于for
循环遍历注意事项会在下面提到。
我们把操作符<-
用在通道的类型字面量中,代表的不是“发送”或“接收”的动作,而是表示通道的方向。
<-
表示了这个通道是单向的,并且只能发而不能收。
var ch1 = make(chan<- int, 1)
ch1 <- 1
fmt.Println(<-ch1) // invalid operation: <-ch1 (receive from send-only type chan<- int)
如果这个操作符紧挨在chan
的左边,那么就说明该通道只能收不能发。
var ch2 = make(<-chan int, 1)
ch2 <- 1 // invalid operation: ch2 <- 1 (send to receive-only type <-chan int)
单向通道最主要的用途就是约束其他代码的行为。比如我们封装一个工具,以通道的方式对外提供使用,只希望别人从通道中读数据,不希望别人从通道中写数据,那么此处就可以使用只读通道。
下面上一段实际使用的demo代码:
// 这个函数中的代码只能向参数ch发送元素值,而不能从它那里接收元素值。这就起到了约束函数行为的作用。
func SendInt(ch chan<- int) {
// 初始化随机数的资源库, 如果不执行这行, 不管运行多少次都返回同样的值
rand.Seed(time.Now().UnixNano())
setNum := rand.Intn(1000)
fmt.Printf("A random number from 1-1000: %v\n", setNum)
ch <- setNum
close(ch)
}
func getIntChan() <-chan int {
num := 3
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}
func main() {
intChan1 := make(chan<- int, 1)
SendInt(intChan1)
fmt.Println("-----------")
intChan2 := make(chan int, 1)
SendInt(intChan2)
for elem2 := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem2)
}
fmt.Println("-----------")
intChan3 := getIntChan()
for elem3 := range intChan3 {
fmt.Printf("The element in intChan3: %v\n", elem3)
}
}
代码输出结果是:
A random number from 1-1000: 281
-----------
A random number from 1-1000: 799
The element in intChan2: 799
-----------
The element in intChan3: 0
The element in intChan3: 1
The element in intChan3: 2
看完上面intChan2
的代码,是不是觉得很诡异,在实际场景中,这种约束一般会出现在接口类型声明中的某个方法定义上。
type Notifier interface {
SendInt(ch chan<- int)
}
以上代码Notifier
接口中的SendInt
方法只会接受一个发送通道作为参数,所以,在该接口的所有实现类型中的SendInt
方法都会受到限制。这种约束方式还是很有用的,尤其是在我们编写模板代码或者可扩展的程序库的时候。
同时在调用SendInt函数的时候,只需要把一个元素类型匹配的双向通道传给它就行了,没必要用发送通道,因为 Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。
上述demo
代码中for
语句会不断地尝试从通道intChan3
中取出元素值。即使intChan3
已经被关闭了,它也会在取出所有剩余的元素值之后再结束执行。
通常,当通道intChan3
中没有元素值时,这条for
语句会被阻塞在有for
关键字的那一行,直到有新的元素值可取。不过,由于getIntChan
函数会事先将intChan3
关闭,所以它在取出intChan3
中的所有元素值之后会直接结束执行。
倘若通道intChan3
的值为nil
,那么这条for语句就会被永远地阻塞在有for
关键字的那一行。
select
语句只能与通道联用,它一般由若干个分支组成。每次执行这种语句的时候,一般只有一个分支中的代码会被运行。
由于select
语句是专为通道而设计的,所以每个case
表达式中都只能包含操作通道的表达式,比如接收表达式。
select语句的分支分为两种,一种叫做候选分支case <-intChannels[0]:
,另一种叫做默认分支default:
。
接下来我们上一段代码:
func main() {
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
for i := 0; i < 2; i++ {
// 随机选择一个通道,并向它发送元素值。
rand.Seed(time.Now().UnixNano())
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
}
}
执行结果:
The index: 1
The second candidate case is selected.
The index: 2
The third candidate case is selected, the element is 2.
上述示例代码写了默认分支,无论涉及通道操作的表达式是否有阻塞,select
语句不会被阻塞。
如果没有加入默认分支,一旦所有的case
表达式都没有满足求值条件,select
语句就会被阻塞,直到至少有一个case
表达式满足条件为止。
通道可能在select
的时候已经关闭了,而直接从通道接收到一个其元素类型的零值。我们可以通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。
在for循环中使用select
的时候,我们在select
中使用break
只会跳出这个select
而不会跳出for
循环,需要重点注意,防止死循环。
下面我们继续看一段相关的示例代码:
func main() {
intChan := make(chan int, 1)
// 一秒后关闭通道。
time.AfterFunc(time.Second, func() {
close(intChan)
})
select {
case _, ok := <-intChan:
if !ok {
fmt.Println("The candidate case is closed.") // Execute this
break
}
fmt.Println("The candidate case is selected.")
}
}
如果select
语句发现同时有多个候选分支满足选择条件,它就会用一种伪随机的算法在这些分支中选择一个并执行。注意,即使select
语句是在被唤醒时发现的这种情况,也会这样做。
如果在select语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
// 把这个channel重新赋值成为一个长度为0的非缓冲通道,这样这个case就一直被阻塞了
for {
select {
case _, ok := <-ch1:
if !ok {
ch1 = make(chan int)
}
case ..... :
default:
}
}
在select语句与for语句联用时,怎样直接退出外层的for语句?
// 方案1:break配合标签
ch1 := make(chan int, 1)
time.AfterFunc(time.Second, func() { close(ch1) })
loop:
for {
select {
case _, ok := <-ch1:
if !ok {
break loop
}
fmt.Println("ch1")
}
}
fmt.Println("END")
// 方案2:goto配合标签
ch1 := make(chan int, 1)
time.AfterFunc(time.Second, func() { close(ch1) })
for {
select {
case _, ok := <-ch1:
if !ok {
goto loop
}
fmt.Println("ch1")
}
}
loop:
fmt.Println("END")
#Reference:
极客时间 - Go语言核心36讲
https://blog.csdn.net/weixin_42117918/article/details/82055634
https://www.cnblogs.com/lianggx6/p/12558663.html