管道是Go在语言层面提供的协程间的通信方式。
声明和初始化管道的方式主要有以下几种:
var ch chan int //声明管道
这种方式声明的管道,值为nil。每个管道只能存储一种类型数据。
使用内置函数可以创建为无缓冲管道和带缓冲管道
ch1 := make(chan string) //无缓冲管道
ch2 := make(chan string, 5) //带缓冲通道
操作符 < -
表示数据流向,管道在左表示向管道写入数据,管道在左表示向管道写入数据,管道在右表示从管道读出数据
ch := make(chan int,10)
ch <- 1 // 数据流入管道
d := <-ch // 数据流出管道
fmt.Println(d)
默认的管道为双向可读写,管道在函数间传递时可以使用操作符限制管道读写
func ChanParamRW(ch chan int) {
// 管道可读写
}
func ChanParamR(ch <-chan int) {
// 只能从管道读取数据
}
func ChanParamW(ch chan<- int) {
// 只能向管道写入数据
}
下面以读取数据为例:
对于值为nil的通道,无论读写都会阻塞,而且是永久阻塞。使用内置函数close() 可以关闭管道,尝试向关闭的管道写入数据会触发panic,但关闭的管道仍可读。
管道读取表达式最多可以给两个变量赋值:
v1 := <-ch
x,ok := <-ch
第一个变量表示读出的数据,第二个变量(bool 类型) 表示是否成功读取了数据(不表示管道的关闭状态)。
一个已关闭的管道有两种情况:
内置函数 len() 和 cap() 作用于管道,分别用于查询缓冲区中数据个数及缓冲区大小。管道实现了一种FIFO(先入先出)的队列,数据总是按照写入的顺序流出管道。
协程读取管道时,阻塞的条件有:
协程写入管道时,阻塞的条件有:
管道数据结构源码位于源码包src/runtime/chan.go文件中
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示下一个被读取元素在队列中的位置
recvq waitq // 等待读消息的协程队列
sendq waitq // 等待写消息的协程队列
lock mutex // 互斥锁,chan不允许并发读写
}
chan内部实现了一个环形队列作为其缓冲区,队列的容量是创建chan时指定的。
从管道读取数据时,如果管道缓冲区为空或者没有缓冲区,则当前协程会被阻塞,并被加入recvq队列。向管道写数据时,如果管道缓冲区为空或者没有缓冲区,则当前协程会被阻塞,并被加入sendq队列。
一个管道只能传递一种类型的值,类型信息存储在hchan数据结构中
一个管道同时仅允许被一个协程读写。
创建管道的过程实际上时初始化hchan结构,其中类型信息和缓冲区长度由内置函数make()指定,buf的大小由元素大小和缓冲区长度共同决定。
向一个管道中写数据的过程如下:
从一个管道读数据的简单过程如下:
关闭通道时会把recvq中的协程全部唤醒,这些写成获取的数据都为对应类型的零值。同时还会把sendq队列中的协程全部唤醒,但这些协程会触发painc。
除此之外,其他会触发panic的操作的还有:
由管道的数据结构我们知道,实际上并没有单向管道。单向管道只是对管道的一种使用限制。
cha 是一个正常的管道,而ChanParamR()参数限制了传入的管道只能用来读,ChanParamW()参数限制了传入的管道只能用来写。
使用select可以监控多个管道,当其中某一个管道可操作时就会触发相应的case分支。select的case语句读挂电脑不会被阻塞,尽管管道中没有数据。这是由于case语句编译后调用管道时会明确传入不阻塞的参数,读不到数据时不会将当前协程加入等待队列,而是直接返回。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
go ChanParamW(ch1)
go ChanParamW(ch2)
for {
select {
case e := <-ch1:
fmt.Println("ch1: ", e)
case e := <-ch2:
fmt.Println("ch2: ", e)
default:
fmt.Println("通道 ch1 和 ch2 中无元素")
time.Sleep(1 * time.Second)
}
}
}
func ChanParamW(ch chan<- int) {
for {
ch <- 1
time.Sleep(1 * time.Second)
}
}
通过for-range可以持续从管道中读出数据,当管道中没有数据时会阻塞当前协程。
func chanRange(chanName chan int) {
for e := range chanName {
fmt.Println(e)
}
}