基本用法
chan string // 可以发送接收string
chan<- struct{} // 只能发送struct{}
<-chan int // 只能从chan接收int
这个箭头总是射向左边的,元素类型总在最右边。如果箭头指向 chan,就表示可以往 chan 中塞数据;如果箭头远离 chan,就表示 chan 会往外吐数据。
基本使用也就这 3 个,说点其他操作。
其他操作
- 是忽略读取的值,只是清空 chan:
for range ch {}
- send 和 recv 都可以作为 select 语句的 case clause:
func main() {
var ch = make(chan int, 10)
for i := 0; i < 10; i++ {
select {
// 向 chan push data
case ch <- i:
// 向 chan pull data,并赋值 =>v
case v := <-ch:
fmt.Println(v)
}
}
}
实现原理
从数据结构,初始化,以及 send, recv, close
,三个惯常操作来看看 channel
的底层实现。
数据结构
chan 的数据结构如下图所示,数据类型是 runtime.hchan
type hchan struct {
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
closed uint32
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 mutex
}
- qcount:chan 中已经接收但还没被取走的元素的个数。值:
len(ch)
。 - dataqsiz:循环队列的大小。
- buf:存放元素的循环队列的 buffer。
- elemtype & elemsize:chan 中元素的 type 和 size(是元素本身的size)。
- sendx:发送数据的指针在 buf 中的位置。
- recvx:处理接收请求时的指针在 buf 中的位置。一旦取出数据,此指针会移动到下一个位置。
- recvq:如果消费者因为没有数据可读而被阻塞了,就会被加入到 recvq 队列中。
- sendq:如果生产者因为 buf 满了而阻塞,会被加入到 sendq 队列中。
- lock:加锁能保护hchan的所有字段,包括waitq中sudoq对象。
初始化
Go 在编译的时候,会根据容量的大小选择调用 makechan64,还是 makechan。
关注 makechan 就好了,因为 makechan64 只是做了 size 检查,底层还是调用 makechan 实现的。 makechan 的目标就是生成 hchan 对象。
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 略去检查代码
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
//
var c *hchan
switch {
case mem == 0:
// chan的size或者元素的size是0,不必创建buf
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不是指针,分配一块连续的内存给hchan数据结构和buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
// hchan数据结构后面紧接着就是buf
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针,那么单独分配buf
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 元素大小、类型、容量都记录下来
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
return c
}
最终,针对不同的容量和元素类型,这段代码分配了不同的对象来初始化 hchan 对象的字段,返回 hchan 对象。
send
Go 在编译发送数据给 chan 的时候,会把 send 语句转换成 chansend1 函数,chansend1 函数会调用 chansend:
send() -> chansend1() -> chansend()
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 第一部分
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable") // L11
}
......
}
第一部分: 如果 chan 是 nil 的话,就把调用者
goroutine park(阻塞休眠)
, 调用者就永远被阻塞住了,所以,第 11 行是不可能执行到的代码。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
...
// 第二部分,如果chan没有被close,并且chan满了,直接返回
if !block && c.closed == 0 && full(c) {
return false
}
...
}
第二部分: 是当你往一个已经满了的 chan 示例发送数据时,并且想不阻塞当前调用,那么这里的逻辑是直接返回。chansend1 方法在调用 chansend 的时候设置了阻塞参数,所以不会执行到第二部分的分支里。