在之前的文章中,我们有介绍过 channel 的使用,传送门。比较经典的一句话就是:
❝在 Go 语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过 channel 发送接收消息的方式进行数据传递。 ❞
这里我们再更加深层次的了解下 chan 。
src/runtime/chan.go
中定义了 channel 的数据结构如下:
type hchan struct {
qcount uint // 队列中的总元素个数
dataqsiz uint // 环形队列大小,即可存放元素的个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 //每个元素的大小
closed uint32 //标识关闭状态
elemtype *_type // 元素类型
sendx uint // 发送索引,元素写入时存放到队列中的位置
recvx uint // 接收索引,元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex //互斥锁,chan不允许并发读写
}
chan 内部实现了一个环形队列来作为缓冲区,队列的长度是在创建 chan 的时候所指定的。
如下图,我们创建一个可缓存6个元素的 channel 示意图:
[0,6)
;[0,6)
;如下图,为没有缓冲区的 channel,recvq
中有几个 goroutine 阻塞等待读数据。
❝注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向 channel一边写数据,一边读数据。 ❞
一个 channel 只能传递一种类型的值,类型信息存储在 hchan 数据结构中:
我们知道,channel 是并发安全的,即一个channel同时仅允许被一个goroutine读写。
创建 channel 其实就是初始化 hchan 结构,其类型信息和缓冲区长度由 make 语句传入,buf 的大小则与元素大小和缓冲区长度来共同决定。
创建 channel 的伪代码:
func makechan(t *chantype, size int) *hchan{
var c *hchan
c = new(hchan)
c.buf = malloc(元素类型大小*size)
c.elemsize = 元素类型大小
c.elemtype = 元素类型
c.dataqsiz = size
return c
}
过程如下:
过程如下:
关闭 channel 时会将 recvq 中的 G 全部唤醒,,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。
panic 出现的场景还有:
只能发送或者只能接收的 channel 为单向 channel。
只需要在基础声明中增加操作符即可:
send := make(ch<- int) //只能发送数据给channel
receive := make(<-ch int) //只能从channel中接收数据
示例:
package main
import (
"fmt"
)
//只能发送通道
func send(s chan<- string){
s <- "微客鸟窝"
}
//只能接收通道
func receive(r <-chan string){
str := <-r
fmt.Println("str:",str)
}
func main() {
//创建一个双向通道
ch := make(chan string)
go send(ch)
receive(ch)
}
//运行结果: str: 微客鸟窝
select 可以实现多路复用,即同时监听多个 channel。
示例:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
fmt.Println("--", i)
}
}
}
运行结果:
-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8
❝select 的 case 语句读 channel 不会阻塞,尽管 channel 中没有数据。这是由于 case 语句编译后调用读 channel 时会明确传入不阻塞的参数,此时读不到数据时不会将当前 goroutine 加入到等待队列,而是直接返回。 ❞
通过 range 可以持续从 channel 中读取数据,类似于遍历,当 channel 中没有数据时会阻塞当前 goroutine ,与读 channel 时阻塞处理机制一样。
示例:
for ch := range chanName {
fmt.Printf("chan: %d\n", ch)
}
❝注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。 ❞
有什么问题,可以公众号内回复或加我微信交流。