1个数据缓存+2个协程队列
解释:
数据缓存:是一个环形队列,存放具体数据,是int还是bool还是结构体
协程队列包含发送写协程队列和读协程队列
写协程队列,:当数据缓存满时,写操作会阻塞,并放入写协程队列
读协程队列:当数据缓存空时,读操作会阻塞,并放入读协程队列.
原理就是很简单,具体结构在 runtime.hchan中
当我们创建 var c = make(chan boo,2l)时
1.有缓存则放缓存
2.缓存没位置则放sendq,等待唤醒
1.sendq不为空,缓存不为空,先拿缓存的,再从sendq拿出一个数据,放到缓存,唤醒之前阻塞的g
2.sendq不为空,缓存为空,说明缓存长度是0, 直接取出g,拿走,唤醒.
3.sendq为空,缓存不为空,从缓存拿数据走人
4.sendq为空,缓存为空,说明没人放东西,阻塞
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:
-
panic: send on closed channe
注意:读不会,读的时候,1.如果缓存有值,则返回缓存数据 2.缓存无值,会返回零值.
有个block的参数可以控制
比如 select操作,编译时候就翻译成 not block了
规则:读取时,如果读不到,会一直阻塞. 当被close掉,会解除阻塞状态.
其实都是语法糖,你普通读,读不到也是阻塞,当close掉,先读缓存,缓存为空返回零值.
只不过range,在close掉后,不会进入到range的业务代码里.直接跳过去.
相当于:
- for true {
- data, ok := <-a
- if ok {
- fmt.Println(data)
- } else {
- break
- }
- }