数据结构
type hchan struct {
// 当前缓冲区中元素的个数
qcount uint
// 环形缓冲区的大小
dataqsiz uint

// 缓冲区数组的起始地址
buf unsafe.Pointer
// 缓冲去的元素大小
elemsize uint16
// 缓冲区的元素类型
elemtype *_type
// 判断当前 channel 是否已经关闭
closed uint32

// 数据在缓冲区的索引
sendx uint
recvx uint

// 正在阻塞等待的 goroutine 队列;
// 大多数情况下 recvq 和 sendq 队列上至少有一个队列是空的
// 有一种特殊情况就是无缓冲的 channel,被单个 goroutine 通过 select 对该 channel 同时发送和接收数据,此时 channel 的 recvq 和 sendq 的长度分别由 select 上 的对 channel 操作的 case 数量决定, recvq 和 sendq 都是非空的状态(Question=>这段内容为原文的注释,目前我也不太理解,待看 select 具体实现时在分析)
recvq waitq
sendq waitq

lock mutex
}

// 双向指针实现的队列
type waitq struct {
first *sudog
last *sudog
}


初始化
// 文件路径:{GOROOT}/src/runtime/chan.go
func makechan(t *chantype, size int) *hchan {
elem := t.elem
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 计算缓冲区的内存大小 mem = 元素的大小 * 缓冲区的元素个数
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
// 分配内存大小
switch {
// 无缓冲区
case mem == 0:
c = (*hchan)(mallocgc(hchanSize, nil, true))
// 调试用的,不需要管
c.buf = c.raceaddr()

// 带有缓冲区的,且缓冲区元素是非指针类型的
// 此时该 channel 的内存分布为 hchan 的固定大小 + 缓冲区的大小 是一段连续的地址
case elem.ptrdata == 0:
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
// 移动指针偏移量 hchanSize, 使其指向缓冲区的起始地址
c.buf = add(unsafe.Pointer(c), hchanSize)

// 带有缓冲区,而且缓冲区的元素是指针类型的;大小也是 hchan 的固定大小 + 缓冲区大小,但是是一段非连续的地址空间
default:
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// QUESTION => 为什么元素类型为非指针类型的 hchan 的内存是一段连续的地址
// 而元素类型是指针的的 hchan 是一段非连续的地址空间?这个不太理解,望大佬告知
// 初始化其他的一些字段
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)

if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}

往 channel 发送数据
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 往一个空的 channel 发送数据
if c == nil {
// 非阻塞的发送数据(在 select 语句中会存在这样的情景),
// 直接就返回了失败
if !block {
return false
}
// gopark 主用用于将当前 goroutine 状态从 _Grunning => _Gwaiting, 退让线程 m 的使用权,并等待唤醒;由于这里传入的 unlockf 回调函数为空,将会导致该挂起的 goroutine 将永远无法被唤起。当进程中正在运行的 m 为0时将会因为死锁而结束进程
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}

if debugChan {
print("chansend: chan=", c, "\n")
}

if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
}
// 缓冲区已经满了,非阻塞发送数据直接返回失败
// 注意此时没有对 channel 加锁的,该判断不一定正确!
// Fast path: check for failed non-blocking operation without acquiring the lock.
if !block && c.closed == 0 && full(c) {
return false
}

var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}

lock(&c.lock)

if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 如果此时 recvq 队列中有 goroutine 在等待接收数据,直接将数据发送到该 goroutine 上,没必要将数据写到 buf 了。
// 同时还会调用 goready() 将因等待接收数据而阻塞的 goroutine 唤醒(_Gwaiting => _Grunnable),并将该 goroutine
// 放到 p 的 runnext 中等待调度器的下次执行
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// channel 的缓冲区未满,可以往缓冲区写入数据
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 非阻塞模式下:
// 第一次判断能否向 channel 发送数据时,是没有加锁的,并不能保证当时那个状态是可靠的,因此加锁之后,需要再次判断;如果不能插入数据
// 返回失败
if !block {
unlock(&c.lock)
return false
}

// goroutine 无法向 channel 发送数据,开始阻塞等待,直到接收者来唤醒
gp := getg()
// 获取一个 sudog,并初始化一些字段,将该 sudog 入队到 channel 的 send 队列中;排队等待 recv goroutine 的唤醒
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}

mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)

// QUESTION=> 这个操作和回调函数 chanparkcommit,我没太理,希望知道的大佬说下。
atomic.Store8(&gp.parkingOnChan, 1)
// 将 send goroutine 挂起等待唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

// 确保正在发送的数据被接收者完全接收之前,数据是存活的,防止被 GC 清理掉
KeepAlive(ep)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
// 执行一些收尾的动作,释放资源。
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}

示意图

场境一:缓冲区中无数据,recvq 上有阻塞等待的任务


go channel 的实现(一)_golang

场景二:recvq 等待队列上没有等待接收数据的任务,channel 缓冲区有数据



go channel 的实现(一)_golang_02


场景三:缓冲区数据已满


go channel 的实现(一)_golang_03