在 Go 语言中,Channel(通道) 是并发编程的核心组件之一。如果说 Goroutine 是 Go 并发执行的实体,那么 Channel 就是这些实体之间沟通的桥梁。
Go 语言有一句非常经典的并发哲学:
"Do not communicate by sharing memory; instead, share memory by communicating." > (不要通过共享内存来通信,而应该通过通信来共享内存。)
1. Channel 的基本操作

2. 无缓冲 vs 有缓冲
这是理解 Channel 行为最关键的地方,它们的阻塞机制完全不同。
无缓冲 Channel (同步通信)
默认通过 make(chan T) 创建的都是无缓冲 Channel。
-
特性: 它没有内部容量。发送方和接收方必须同时准备好,数据才能传递成功。
-
阻塞场景: * 如果发送方先执行
ch <- data,它会被阻塞 ,直到有另一个 Goroutine 执行<-ch。- 反之,如果接收方先执行
<-ch,它也会被阻塞,直到有 Goroutine 向其发送数据。
- 反之,如果接收方先执行
-
比喻: 就像接力赛跑,交棒人和接棒人必须同时在交接区,棒子才能递过去。
有缓冲 Channel (异步通信)
通过 make(chan T, capacity) 创建,带有初始容量。
-
特性: 内部有一个队列(Buffer)。只要队列没满,发送方就可以一直发送;只要队列没空,接收方就可以一直接收。
-
阻塞场景:
-
发送方阻塞: 只有当缓冲区已满时,继续发送才会阻塞。
-
接收方阻塞: 只有当缓冲区为空时,继续接收才会阻塞。
-
-
比喻: 就像快递柜。快递员(发送方)只要看到有空柜子就可以把件放进去(不阻塞),不需要等客户。只有快递柜满了,快递员才需要等。客户(接收方)只要柜子里有自己的件就能取,只有柜子空了才需要等。
简单来说,Channel 的底层是一个带锁的环形队列(Ring Buffer) ,外加两个用于存放阻塞 Goroutine 的双向链表 ,并且深度集成了 Go 的 GMP 调度器。

2. 发送数据的底层逻辑 (
ch <- data)当一个 Goroutine 执行发送操作时,底层会按照以下顺序进行判断和处理:
直接交接(Direct Handoff - 性能最高):
检查
recvq(接收等待队列)是否为空。如果不为空 ,说明有接收者正在苦苦等待。此时,Go 会做一件非常巧妙的事:直接将数据从发送方 Goroutine 的栈内存,拷贝到接收方 Goroutine 的栈内存中,然后唤醒那个睡眠的接收方 Goroutine。
优势: 完全绕过了
buf,减少了一次内存拷贝,效率极高。写入缓冲区(Write to Buffer):
如果
recvq为空,并且 Channel 有缓冲区且未满(qcount < dataqsiz)。发送方会把数据拷贝到环形队列
buf的sendx位置,然后将sendx向前移动一格,qcount加一。整个过程结束。阻塞睡眠(Block):
如果缓冲区也满了,或者根本就是无缓冲 Channel。
发送方 Goroutine 会被打包成一个
sudog节点,挂载到sendq(发送等待队列)中。然后调用
gopark让出 CPU 的执行权,当前 Goroutine 进入休眠,等待被别人唤醒。3. 接收数据的底层逻辑 (
<-ch)接收逻辑与发送逻辑基本是对称的:
直接交接或满缓冲拉取:
检查
sendq是否有等待的发送者。如果有,且是无缓冲 Channel:直接从发送者的栈上把数据拷贝过来,然后唤醒发送者。
如果有,且是有缓冲 Channel(说明缓冲区满了):先从环形队列的头部(
recvx)取走一个数据,然后把sendq中那个等待的发送者的数据放入队列的尾部(sendx),最后唤醒发送者。从缓冲区读取:
如果没有人在等待发送,且缓冲区里有数据(
qcount > 0)。从
buf的recvx位置读取数据,recvx向前移动一格,qcount减一。阻塞睡眠:
如果缓冲区为空,或者无缓冲且没有发送者。
接收方 Goroutine 被打包成
sudog,放入recvq队列,调用gopark休眠,等待发送者带着数据来唤醒它。
Go
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //带有缓冲的channel 缓冲空间为3
fmt.Println("c is ", c, " len is ", len(c), "cap is", cap(c))
go func() {
defer fmt.Println("goroutine end")
for i := 0; i < 10; i++ {
c <- i
fmt.Println("子go在运行,值为", i, "len is", len(c), "cap is", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 10; i++ {
num := <-c
fmt.Println("main中的管道值接收到了,值为", num)
}
fmt.Println("main end")
}
GOROOT=D:\Code_software\Go_software #gosetup
GOPATH= #gosetup
D:\Code_software\Go_software\bin\go.exe build -o C:\Users\weixu\AppData\Local\JetBrains
\GoLand2025.3\tmp\GoLand\___go_build_test_channel1_go.exe D:\Code_software\Go_software\
Project_go_25_12_30\test-goroutine\test-channel1.go #gosetup
C:\Users\weixu\AppData\Local\JetBrains\GoLand2025.3\tmp\GoLand\___go_build_test_
channel1_go.exe #gosetup
c is 0xc0000a2000 len is 0 cap is 3
子go在运行,值为 0 len is 1 cap is 3
子go在运行,值为 1 len is 2 cap is 3
子go在运行,值为 2 len is 3 cap is 3
main中的管道值接收到了,值为 0
main中的管道值接收到了,值为 1
main中的管道值接收到了,值为 2
main中的管道值接收到了,值为 3
子go在运行,值为 3 len is 0 cap is 3
子go在运行,值为 4 len is 0 cap is 3
子go在运行,值为 5 len is 1 cap is 3
子go在运行,值为 6 len is 2 cap is 3
子go在运行,值为 7 len is 3 cap is 3
main中的管道值接收到了,值为 4
main中的管道值接收到了,值为 5
main中的管道值接收到了,值为 6
main中的管道值接收到了,值为 7
main中的管道值接收到了,值为 8
子go在运行,值为 8 len is 0 cap is 3
子go在运行,值为 9 len is 0 cap is 3
goroutine end
main中的管道值接收到了,值为 9
main end
Process finished with the exit code 0
3、channel与range

4、select的使用

5、go之间通信
通过函数参数显式传递 (最推荐)
Go
package main
import "fmt"
// worker 函数接收一个专门处理 int 的 channel
func worker(id int, ch chan int) {
// 因为 main 函数把 ch 的指针传进来了,所以它明确知道往哪个 channel 发数据
ch <- id * 10
}
func main() {
// 1. 创建一根实物水管 (底层是在堆上分配了 hchan,ch 拿到了内存地址)
myChannel := make(chan int)
// 2. 启动多个 Goroutine,并把这根水管的地址"递"给它们
go worker(1, myChannel)
go worker(2, myChannel)
// 3. 从这根水管里取两次数据
fmt.Println(<-myChannel)
fmt.Println(<-myChannel)
}