信道(channel)(又称为通道) ,就是一个管道,可以想像成 Go 协程之间通信的管道。它是一种队列式的数据结构,遵循先入先出的规则。
信道的声明
每个信道都只能传递一种数据类型的数据,在你声明的时候,我们要指定信道的类型。chan Type
表示 Type
类型的信道。信道的零值为 nil
。
var channel_name chan channel_type
下面的语句声明了一个类型为 int
的信道 a
,该信道 a
的值为 nil
。
var a chan int
信道的初始化
声明完信道后,信道的值为 nil
,我们不能直接使用,必须先使用 make
函数对信道进行初始化操作。
channel_name = make(chan channel_type)
使用下面的语句我们可以对上面声明过的信道 a
进行初始化:
a = make(chan int)
这样,我们就已经定义好了一个 int
类型的信道 a
。
当然,也可以使用下面的简短声明语句一次性定义一个信道:
a := make(chan int)
使用信道发送和接收数据
往信道发送数据使用的是下面的语法:
// 把 data 数据发送到 channel_name 信道中
// 即把 data 数据写入到 channel_name 信道中
channel_name <- data
从信道接收数据使用的是下面的语法:
// 从 channel_name 信道中接收数据到 value
// 即从 channel_name 信道中读取数据到 value
value := <- channel_name
信道旁的箭头方向指定了是发送数据还是接收数据。箭头指向信道,代表数据写入到信道中;箭头往信道指向外,代表从信道读数据出去。
下面的例子演示了信道的使用:
package main
import (
"fmt"
)
func John(c chan string) {
// 往信道传入数据 "John: Hi"
c <- "John: Hi"
}
func main() {
// 创建一个信道
c := make(chan string)
// 打印 "Mary: Hello"
fmt.Println("Mary: Hello")
// 开启协程
go John(c)
// 从信道接收数据
rec := <- c
// 打印从信道接收到的数据
fmt.Println(rec)
// 打印 "Mary: Nice to meet you"
fmt.Println("Mary: Nice to meet you")
}
该程序模仿了两个人见面的场景,在 main
函数中,创建了一个信道,在 main
函数中先打印了 Mary: Hello
,然后开启协程运行 John
函数,而 main
函数通过协程接收数据,主协程发生了阻塞,等待信道 c
发送的数据,在函数中,数据 John: Hi
传入信道中,当写入完成时,主协程接收了数据,解除了阻塞状态,打印出从信道接收到的数据 John: Hi
,最后打印 Mary: Nice to meet you
。运行该程序输出如下:
Mary: Hello
John: Hi
Mary: Nice to meet you
发送与接收默认是阻塞的
从上面的例子我们知道,如果从信道接收数据没接收完主协程是不会继续执行下去的。当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。
信道的关闭
对于一个已经使用完毕的信道,我们要将其进行关闭。
close(channel_name)
这里要注意,对于一个已经关闭的信道如果再次关闭会导致报错,我们可以在接收数据时,判断信道是否已经关闭,从信道读取数据返回的第二个值表示信道是否没被关闭,如果已经关闭,返回值为 false
;如果还未关闭,返回值为 true
。
value, ok := <- channel_name
信道的容量与长度
我们在前面讲过 make
函数是可以接收两个参数的,同理,创建信道可以传入第二个参数——容量。
- 当容量为
0
时,说明信道中不能存放数据,在发送数据时,必须要求立马有人接收,否则会报错。此时的信道称之为无缓冲信道。 - 当容量为
1
时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。利用这点可以利用信道来做锁。 - 当容量大于
1
时,信道中可以存放多个数据,可以用于多个协程之间的通信管道,共享资源。
既然信道有容量和长度,那么我们可以通过 cap
函数和 len
函数获取信道的容量和长度。
package main
import (
"fmt"
)
func main() {
// 创建一个信道
c := make(chan int, 3)
fmt.Println("初始化后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
c <- 1
c <- 2
fmt.Println("传入两个数后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
<- c
fmt.Println("取出一个数后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
}
程序中 <- c
通过信道接收数据但没有存入变量中也是合法的,运行该程序后输出如下:
初始化后:
cap = 3
len = 0
传入两个数后:
cap = 3
len = 2
取出一个数后:
cap = 3
len = 1
缓冲信道与无缓冲信道
按照是否可缓冲数据可分为:缓冲信道 与 无缓冲信道 。
无缓冲信道在信道里无法存储数据,接收端必须先于发送端准备好,以确保你发送完数据后,有人立马接收数据,否则发送端就会造成阻塞,原因很简单,信道中无法存储数据。也就是说发送端和接收端是同步运行的。
c := make(chan int)
// 或者
c := make(chan int, 0)
缓冲信道允许信道里存储一个或多个数据,设置缓冲区后,发送端和接收端可以处于异步的状态。
c := make(chan int, 3)
双向信道
到目前为止,上面定义的都是双向信道,既可以发送数据也可以接收数据。例如:
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个信道
c := make(chan int)
// 发送数据
go func() {
fmt.Println("send: 1")
c <- 1
}()
// 接收数据
go func() {
n := <- c
fmt.Println("receive:", n)
}()
// 主协程休眠
time.Sleep(time.Millisecond)
}
运行上面的程序输出如下:
send: 1
receive: 1
单向信道
单向信道只能发送或者接收数据。所以可以具体细分为只读信道和只写信道。
<-chan
表示这个信道,只能发送出数据,即只读:
// 定义只读信道
c := make(chan int)
// 定义类型
type Receiver = <-chan int
var receiver Receiver = c
// 或者简单写成下面的形式
type Receiver = <-chan int
receiver := make(Receiver)
chan<-
表示这个信道,只能接收数据,即只写:
// 定义只写信道
c := make(chan int)
// 定义类型
type Sender = chan<- int
var sender Sender = c
// 或者简单写成下面的形式
type Sender = chan<- int
sender := make(Sender)
下面是一个例子:
package main
import (
"fmt"
"time"
)
// Sender 只写信道类型
type Sender = chan<- string
// Receiver 只读信道类型
type Receiver = <-chan string
func main() {
// 创建一个双向信道
var pipe = make(chan string)
// 开启一个协程
go func() {
// 只能写信道
var sender Sender = pipe
fmt.Println("send: 2333")
sender <- "2333"
}()
// 开启一个协程
go func() {
// 只能读信道
var receiver Receiver = pipe
message := <-receiver
fmt.Println("receive: ", message)
}()
time.Sleep(time.Millisecond)
}
运行上面的程序输出如下:
send: 2333
receive: 2333
遍历信道
使用 for range
循环可以遍历信道,但在遍历时要确保信道是处于关闭状态,否则循环会被阻塞。
package main
import (
"fmt"
)
func f(c chan int) {
for i := 0; i < 10; i++ {
c <- i
}
// 记得要关闭信道
// 否则主协程遍历完不会结束,而会阻塞
close(c)
}
func main() {
// 创建一个信道
var pipe = make(chan int, 5)
go f(pipe)
for v := range pipe {
fmt.Println(v)
}
}
运行上面的程序输出数字 0
至 9
。
用信道做锁
上面讲过,当信道容量为 1
时,说明信道只能缓存一个数据,若信道中已有一个数据,此时再往里发送数据,会造成程序阻塞。例如:
package main
import (
"fmt"
"time"
)
// 由于 x = x+1 不是原子操作
// 所以应避免多个协程对 x 进行操作
// 使用容量为 1 的信道可以达到锁的效果
func increment(ch chan bool, x *int) {
ch <- true
*x = *x + 1
<- ch
}
func main() {
pipe := make(chan bool, 1)
var x int
for i := 0; i < 10000; i++ {
go increment(pipe, &x)
}
time.Sleep(time.Millisecond)
fmt.Println("x =", x)
}
运行该程序输出如下:
x = 10000
但如果把容量改大,例如 1000
,可能输出的数值会小于 10000
。
死锁
讲完了锁,不得不提死锁。当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic
,形成死锁。同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序也会触发 panic
。
package main
func main() {
ch := make(chan bool)
ch <- true
}
运行上面的程序,会触发 panic ,报下面的错误:
fatal error: all goroutines are asleep - deadlock!
下面再来看看几个例子。
package main
import "fmt"
func main() {
ch := make(chan bool)
ch <- true
fmt.Println(<-ch)
}
上面的代码你看起来可能觉得没啥问题,创建一个信道,往里面写入数据,再从里面读出数据,但运行后会报同样的错误:
fatal error: all goroutines are asleep - deadlock!
那么为什么会出现死锁呢?前面的基础学的好的就不难想到使用 make
函数创建信道时默认不传递第二个参数,信道中不能存放数据,在发送数据时,必须要求立马有人接收,即该信道为无缓冲信道。所以在接收者没有准备好前,发送操作会被阻塞。
分析完引发异常的原因后,我们可以将代码修改如下,使用协程,将接收者代码放在另一个协程里:
package main
import (
"fmt"
"time"
)
func f(c chan bool) {
fmt.Println(<-c)
}
func main() {
ch := make(chan bool)
go f(ch)
ch <- true
time.Sleep(time.Millisecond)
}
当然,还有一种更加直接的方法,把无缓冲信道改为缓冲信道就行了:
package main
import "fmt"
func main() {
ch := make(chan bool, 1)
ch <- true
fmt.Println(<-ch)
}
有时候我们定义了信道的容量,但信道里的容量已经放不下新的数据,而没有接收者接收数据,就会造成阻塞,而对于一个协程来说就会造成死锁:
package main
import "fmt"
func main() {
ch := make(chan bool, 1)
ch <- true
ch <- false
fmt.Println(<-ch)
}
同理,当程序一直在等待从信道里读取数据,而此时并没有发送者会往信道中写入数据。此时程序就会陷入死循环,造成死锁。
参考文献:
[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程序设计语言, Translated by 李道兵, 高博, 庞向才, 金鑫鑫 and 林齐斌, 机械工业出版社, 2017.