简单概括:
1个数据缓存+2个协程队列
解释:
数据缓存:是一个环形队列,存放具体数据,是int还是bool还是结构体
协程队列包含发送写协程队列和读协程队列
写协程队列,:当数据缓存满时,写操作会阻塞,并放入写协程队列
读协程队列:当数据缓存空时,读操作会阻塞,并放入读协程队列.
原理就是很简单,具体结构在 runtime.hchan中
源码
当我们创建 var c = make(chan boo,2l)时
- qcount 代表当前队列剩余元素个数. 一个没插入,因此是0
- dataqsiz 环形队列长度, 你设置的是2,那么他就是2. 如果是无缓冲的channel,就是0
- bvuf 环形队列指针 指向具体的缓存对象
- elemsize 每个元素大小,int型的就是8字节 ,bool型的就是1字节
- sendx 写入元素位置下标. 你插入一个
- recvx 从队列的该位置读出
- recvq 等待读消息的协程队列
- sendq 等待写消息的协程队列
- lock 互斥锁,不允许并发操作
写入流程:
1.有缓存则放缓存
2.缓存没位置则放sendq,等待唤醒
读取流程
1.sendq不为空,缓存不为空,先拿缓存的,再从sendq拿出一个数据,放到缓存,唤醒之前阻塞的g
2.sendq不为空,缓存为空,说明缓存长度是0, 直接取出g,拿走,唤醒.
3.sendq为空,缓存不为空,从缓存拿数据走人
4.sendq为空,缓存为空,说明没人放东西,阻塞
close流程
1.已经关闭的,再次调用会panic
2.唤醒sendq,触发panic(panic: send on closed channel),
唤醒recvq,设置g的值为nil,用户读到的数据是类型的零值
这里有个点,别混了,如果一个channel关闭了,再去读取,会返回零值么?不是的,会看缓存数据是否为空,不为空,则返回缓存数据内容,为空,则返回零值. 上面这句话,recvq中有协程队列,一个隐含意思就是,你的缓存中已经没有数据了.
那么你可能会有疑问,你读取到了一个类型的零值,你怎么知道是close之后返回的零值,还是说就写入了一个零值?
这个问题和map一样,如果你读取map的key,返回了一个零值,你怎么知道是本身存的零值还是没有key返回的零值呢? 就是加个bool返回值呗,多告诉你点信息.
func TestOk(t *testing.T) {
a := make(chan string, 2)
a <- "a"
close(a) // ① 关闭channel
go func() {
d, ok := <-a
if ok {
fmt.Println("read data: " + d)
}
d2, ok := <-a
if !ok { // ②读取是否成功
fmt.Println("not read data: " + d2)
}
}()
time.Sleep(10 * time.Second)
}
如果没有①的代码. 即不关闭.
输出结果:
read data: a
如果有①的代码.
read data: a
not read data:
其他:
1.channel没有初始化,去读写.会阻塞
2.channel关闭后去写,再次关闭,会引发panic.
panic: send on closed channe
注意:读不会,读的时候,1.如果缓存有值,则返回缓存数据 2.缓存无值,会返回零值.
3.读写channel时一定会阻塞么
有个block的参数可以控制
比如 select操作,编译时候就翻译成 not block了
4.range读取channel
规则:读取时,如果读不到,会一直阻塞. 当被close掉,会解除阻塞状态.
其实都是语法糖,你普通读,读不到也是阻塞,当close掉,先读缓存,缓存为空返回零值.
只不过range,在close掉后,不会进入到range的业务代码里.直接跳过去.
相当于:
for true {
data, ok := <-a
if ok {
fmt.Println(data)
} else {
break
}
}