Golang Channel 底层:hchan、环形队列与等待队列的精密协作

 更新时间:2026年09月11日 09:10:25   作者:FfHUCisI  
Go Channel的底层核心是hchan结构体‌,它通过‌环形缓冲区‌和‌双向等待队列‌的协同,实现了发送方与接收方的精准对接,‌‌本文给大家介绍Golang Channel底层:hchan、环形队列与等待队列的精准握手,感兴趣的朋友一起看看吧

Channel 是 Go 并发设计的灵魂。表面看它是 chan T,背后却是 runtime.hchan 结构体,配合环形缓冲区、两把等待队列和一把锁,实现了 CSP 模型中“通过通信共享内存”的语义。

一、hchan 结构体

Go 源码中 channel 的底层结构简化如下:

type hchan struct {
	qcount   uint           // 当前队列中的元素个数
	dataqsiz uint           // 环形队列大小(缓冲 channel 的容量)
	buf      unsafe.Pointer // 环形队列指针
	elemsize uint16         // 每个元素大小
	closed   uint32         // 是否已关闭
	elemtype *_type         // 元素类型元数据
	sendx    uint           // 发送索引
	recvx    uint           // 接收索引
	recvq    waitq          // 等待接收的 goroutine 队列
	sendq    waitq          // 等待发送的 goroutine 队列
	lock     mutex          // 保护 hchan 的锁
}

关键洞察:无论缓冲 channel 还是无缓冲 channel,都是同一个 hchan 结构。区别只是 dataqsiz 是否为 0。

二、环形队列

缓冲 channel 的元素存在 buf 指向的连续内存中,sendx 和 recvx 两个下标绕圈移动:

初始:sendx=0, recvx=0, qcount=0
send 1:buf[0]=1, sendx=1, qcount=1
send 2:buf[1]=2, sendx=2, qcount=2
recv 1:v=buf[0], recvx=1, qcount=1
send 3:buf[2]=3, sendx=3(模回0), qcount=2

因为是环形,不需要移动数据,只需要移动下标,入队出队都是 O(1)。

三、等待队列

当 channel 没有空间发送或没有元素接收时,goroutine 不会阻塞 OS 线程,而是被包装成 sudog 结构,挂到 sendq 或 recvq 上,并通过 gopark 让出 CPU。

  • recvq:等待接收的 G。当有人来 send 时,直接把数据复制给这个等待的 G,并唤醒它。
  • sendq:等待发送的 G。当有人来 recv 时,直接从等待的 G 复制数据。

无缓冲 channel 的精髓:发送方和接收方必须同时到位。发送方会把数据直接拷贝到接收方的变量里,不需要经过 buf。

四、关闭 channel 的语义

关闭 channel 会:

  1. 设置 closed = 1。
  2. 唤醒 recvq 中所有等待的 G,让它们收到零值并返回 ok=false。
  3. 唤醒 sendq 中所有等待的 G,触发 panic(向已关闭 channel 发送)。

所以关闭一个已经关闭的 channel、或者关闭后 send,都会 panic。

五、select 的随机性从哪里来

select 在 runtime 层面会先把所有 case 涉及的 channel 都加锁,然后随机打乱 case 顺序遍历。如果有多个 case 就绪,选中顺序是伪随机的。这也是 Go select 设计中的一个重要保证:不会总是优先某个 case。

六、代码实践:用缓冲 channel 验证环形队列行为

下面这个程序创建容量为 3 的 channel,手动发送接收,观察顺序和阻塞。

package main
import (
	"fmt"
	"time"
)
func main() {
	ch := make(chan int, 3)
	// 发送 3 个,不阻塞
	for i := 1; i <= 3; i++ {
		ch <- i
		fmt.Printf("sent %d\n", i)
	}
	// 第 4 个发送会阻塞,除非有 goroutine 接收
	go func() {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("receiver waking up")
		v := <-ch
		fmt.Printf("received %d\n", v)
	}()
	fmt.Println("about to block on 4th send")
	ch <- 4
	fmt.Println("4th send succeeded after receiver took one")
	// 清空剩余
	for v := range ch {
		fmt.Printf("drain %d\n", v)
		if len(ch) == 0 {
			break
		}
	}
}

运行结果:

sent 1
sent 2
sent 3
about to block on 4th send
receiver waking up
received 1
4th send succeeded after receiver took one
drain 2
drain 3
drain 4

七、无缓冲 channel 的精准握手

无缓冲 channel 的 buf 为 nil,dataqsiz 为 0。发送和接收必须配对发生。下面验证无缓冲 channel 的同步性。

package main
import (
	"fmt"
	"sync"
	"time"
)
func main() {
	ch := make(chan int)
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		fmt.Println("sender: preparing data at", time.Now().Format("15:04:05.000"))
		ch <- 42 // 会阻塞,直到接收方准备好
		fmt.Println("sender: handoff complete at", time.Now().Format("15:04:05.000"))
	}()
	go func() {
		defer wg.Done()
		time.Sleep(200 * time.Millisecond)
		fmt.Println("receiver: ready at", time.Now().Format("15:04:05.000"))
		v := <-ch
		fmt.Println("receiver: got", v)
	}()
	wg.Wait()
}

输出显示:发送方在 ch <- 42 处等待,直到 200ms 后接收方才取出数据。

八、关闭 channel 与 range 遍历

package main
import "fmt"
func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	close(ch)
	for v := range ch {
		fmt.Println("range got", v)
	}
	v, ok := <-ch
	fmt.Printf("after close: v=%d ok=%v\n", v, ok)
}

输出:

range got 1
range got 2
after close: v=0 ok=false

range 会在 channel 关闭且没有数据后自然退出,ok=false 是判断 channel 关闭的标准方式。

九、逐条解读

  1. make(chan int, 3) 创建带缓冲的 channel,hchan.dataqsiz = 3。
  2. 前 3 次发送直接写 buf,不会触发 sendq 或阻塞。
  3. 第 4 次发送时 qcount == dataqsiz,发送 G 被 park 进 sendq。
  4. 接收 G 取出 buf[recvx],唤醒 sendq 中的发送 G,后者把 4 写入 buf。
  5. 无缓冲 channel 没有 buf,发送方直接把数据拷贝到接收方栈上的变量。
  6. close(ch) 后,recvq 中的等待者收到零值,sendq 中的等待者 panic。
  7. range ch 在关闭后自动退出,不需要额外判断。

十、小结

概念一句话总结
hchanchannel 的底层结构体,包含 buf、锁、sendq/recvq
环形队列缓冲 channel 的 O(1) 入队出队实现
等待队列发送/接收阻塞时,G 被包装成 sudog 挂起
无缓冲 channel发送方直接把数据拷贝给接收方,实现同步握手
关闭 channel通知接收者无更多数据,继续 send 会 panic
select多 channel 加锁后随机遍历,保证公平性

到此这篇关于Golang Channel 底层:hchan、环形队列与等待队列的精密协作的文章就介绍到这了,更多相关Golang Channel hchan结构体内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • GO语言Defer用法实例分析

    GO语言Defer用法实例分析

    这篇文章主要介绍了GO语言Defer用法,实例分析了Defer的使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • k8s在go语言中的使用及client 初始化简介

    k8s在go语言中的使用及client 初始化简介

    这篇文章主要为大家介绍了k8s在go语言中的使用及client 初始化简介,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • go语言yaml转map、map遍历的实现

    go语言yaml转map、map遍历的实现

    本文主要介绍了go语言yaml转map、map遍历的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • 基于Go语言开发一个Markdown转HTML工具

    基于Go语言开发一个Markdown转HTML工具

    这篇文章主要为大家详细介绍了如何基于Go语言开发一个Markdown转HTML工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-09-09
  • golang 微服务之gRPC与Protobuf的使用

    golang 微服务之gRPC与Protobuf的使用

    这篇文章主要介绍了golang 微服务之gRPC与Protobuf的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • 详解如何在Go语言中生成随机种子

    详解如何在Go语言中生成随机种子

    这篇文章主要为大家详细介绍了如何在Go语言中生成随机种子,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下
    2024-04-04
  • Go语言实现文件上传

    Go语言实现文件上传

    这篇文章主要为大家详细介绍了Go语言实现文件上传,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-07-07
  • golang中连接mysql数据库

    golang中连接mysql数据库

    这篇文章主要介绍了golang中连接mysql数据库的步骤,帮助大家更好的理解和学习go语言,感兴趣的朋友可以了解下
    2020-12-12
  • go语言单例模式(Singleton)实例分析

    go语言单例模式(Singleton)实例分析

    这篇文章主要介绍了go语言单例模式(Singleton),实例分析了单例模式的原理与Go语言的实现技巧,需要的朋友可以参考下
    2015-03-03
  • Go语言TCP从原理到代码实现详解

    Go语言TCP从原理到代码实现详解

    这篇文章主要为大家介绍了Go语言TCP从原理到代码实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08

最新评论