管道是Go语言层面提供的协程间的通信方式.比unix管道更易用也更轻便.
1.初始化:
声明初始化管道的方式:
变量声明.这种方式声明管道.值为nil.并且每个管道只能存储一种类型的数据.
使用内置函数make().这种方式可以声明有缓冲通道和无缓冲通道.
示例:
//变量声明管道.
var ch chan int
//make声明.
//无缓冲管道.
chInt := make(chan int)
//有缓冲管道
chString := make(chan string, 5)
}
2.操作符:
操作符"<-"表示数据流向.管道在左表示向管道写入数据.管道在右表示从管道读取数据.
示例:
//创建一个缓冲区为10的int类型的管道.
chanInt := make(chan int, 10)
//向管道写入数据.
chanInt <- 10
//从管道读取数据.
i := <-chanInt
fmt.Println(i)
}
默认的管道为双向可读写.管道在函数传递时可以使用操作符限制管道读写.
示例:
//管道可读写.
}
func ChanR(ch <-chan int) {
//只能从管道读取数据.
}
func ChanW(ch chan<- int) {
//只能向管道写数据.
}
数据读写:
管道没有缓冲区的时候.从管道读取数据会阻塞.直到有协程向管道中写入数据.类似的往管道中写入数据也会阻塞.直到协程从管道读取数据.
管道有缓冲区时.从管道读取数据.如果缓冲区没有数据也会阻塞.直到有协程写入数据.类似的..向管道写入数据时.如果缓冲区已满.也会阻塞.直到有协程从缓冲区中读出数据.
对于值为nil的管道.无论读写都会阻塞.而且是永久阻塞.
内置函数close()可以关闭通道.尝试向关闭的通道写入会触发panic.但关闭的通道仍可读.
管道读值示例:
//创建一个缓冲区为10的int类型的管道.
chanInt := make(chan int, 10)
//向管道写入数据.
chanInt <- 10
chanInt <- 11
//从管道读取数据.
i := <-chanInt
x, ok := <-chanInt
fmt.Println(i)
fmt.Println(x)
fmt.Println(ok)
}
第一个变量表示读出的数据.第二个变量(bool)表示是否成功读取数据.
注:第二个变量不用于表示指示管道的关闭状态.
第二个变量常常会被错误的理解成管道的关闭状态.这个值确实与管道的关闭状态有关.更确切的是与管道缓冲区是否有数据有关.
关闭通道的两种情况:
情况1:管道关闭缓冲区没有数据.
情况2:管道关闭缓冲区有数据.
对于第一种情况.管道已关闭且缓冲区中没有数据.管道读取表达式返回的第一个变量为相应类型的零值.第二个变量为false.
对于第二种情况.管道已关闭且缓冲区有数据.管道读取表达式返回的第一个变量为相应的类型数据.第二个变量为true.
总结:只有管道关闭且缓冲区没有数据时.管道读取表达式的返回的第二个变量才与管道关闭状态一致.
实现原理:
数据结构:
源码位于src/runtime/chan.go:hchan.
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
synctest bool // true if created in a synctest bubble
closed uint32
timer *timer // timer feeding this chan
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
qcount uint 当前队列中剩余的元素个数.
dataqsiz uint 环形队列长度.
buf unsafe.Pointer 环形队列指针.
elemsize uint16 每个元素的大小.
closed uint32 标识关闭状态.
*elemtype _type 元素类型.
sendx uint 元素写入存放队列的位置.
recvx uint 读取元素在队列中的位置.
recvq waitq 等待读消息的协程队列.
sendq waitq 等待写消息的协程队列.
lock mutex 互斥锁.chan不允许并发读写.
waitq结构:
type waitq struct {
first *sudog
last *sudog
}
环形队列:
chan内部实现了一个环形队列作为缓冲区.队列的长度是创建chan时指定的.
编辑
等待队列:
编辑
因读阻塞的协程会被向管道写数据的协程唤醒.
写阻塞的协程会被向读数据的协程唤醒.
注:
一般不会出现读队列和写队列同时排队的情况.只有一个例外是同一个协程使用
select语句向管道一边写数据.一边读数据.此时管道会有两个队列.
类型信息:
一个管道只能传递一种类型的值.类型信息存储在hchan数据结构中.
如果需要管道存储任意类型数据.可以使用interface{}类型.
互斥锁:
一个管道同时仅允许被一个协程读写.
管道操作:
写入数据:
缓冲区有空余位置.则数据写入缓冲区.结束写入过程.
缓冲区没有空余位置.则将当前协程加入到sendq队列.进入睡眠状态等待读协程
唤醒.
编辑
读取数据:
如果缓冲区有数据.则从缓冲区读取数据.结束读取过程.
缓冲区没数据.则从当前协程加入recvq队列.进入睡眠等待写协程唤醒.
编辑
关闭通道:
关闭通道时会把所有recvq中的协程全部唤醒.这些协程获取的数据都为对应类型的零值.同时还会把sendq队列中的协程全部唤醒.但这些协程会触发panic.
其他panic操作:
关闭值为nil的管道.
关闭已被关闭的通道.
向已关闭的通道写入数据.
常见用法:
单向管道:
只能用于发送或接收数据.(通过上面的数据结构得知.实际没有单向管道.只是通过形参来进行限定.)
//创建一个缓冲区为10的int类型的管道.
chanInt := make(chan int, 10)
readChan(chanInt)
writeChan(chanInt)
}
func readChan(ch <-chan int) {
i := <-ch
fmt.Println(i)
}
func writeChan(ch chan<- int) {
ch <- 10
}
select:
使用select可以监控多个管道.当某个管道可操作时就触发相应的case分支.
//创建一个缓冲区为10的int类型的管道.
ch1 := make(chan int, 10)
ch2 := make(chan int, 10)
go selectChan(ch1)
go selectChan(ch2)
for {
select {
case i := <-ch1:
fmt.Println("ch1", i)
case i2 := <-ch2:
fmt.Println("ch2", i2)
default:
fmt.Println("default")
time.Sleep(time.Second)
}
}
}
func selectChan(ch chan int) {
for {
ch <- 1
time.Sleep(time.Second)
}
}
for-range:
可以持续从管道中读出数据.就像遍历数组一样.当管道中没有数据时会阻塞当前
协程.与读管道阻塞处理机制是一样的.管道关闭.for-range也可以优雅的结束.
for e := range ch {
fmt.Println(e)
}
}
知识就是反反复复的重复.
考虑到公众号文章过于碎片化.正在逐步把所有的知识整理到语雀知识库.后面会公开语雀地址.敬请期待.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路