Go Channel
并发执行中,想要让 goroutine 互相通信,通常会共享某些状态;但共享内存容易引发竞态条件,所以往往需要加锁,性能也会受影响。
Go 更提倡:通过通信来共享内存,而不是通过共享内存来实现通信。channel 就是 goroutine 之间「安全地传值 + 同步」的工具。
go
ch := make(chan int) // 创建 channel(无缓冲)
ch := make(chan T, N) // 创建 channel(有缓冲)
ch <- 1 // 发送
x := <-ch // 接收
close(ch) // 关闭
什么时候会被用到
- 数据传递:两个协程之间传数据
- 事件通知:等待某个任务完成
- 生产者 / 消费者:是持续的,不是一次;生产、消费速度可能不一样
- 限制并发数
go
sem := make(chan struct{}, 3)
for i := 0; i < 10; i++ {
sem <- struct{}{} // acquire
go func(i int) {
defer func() { <-sem }() // release
fmt.Println(i)
time.Sleep(time.Second)
}(i)
}
- 多路复用与超时控制
go
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-quit:
return
}
}
- 任务取消:用"关闭一个 channel"来把取消通知广播给一堆正在工作的 goroutine
go
done := make(chan struct{}) // 取消信号
go func() {
for {
select {
case <-done:
return // 收到取消:立刻退出
default:
// 做自己的工作
}
}
}()

Channel数据结构
可以把 channel 想成一个"带缓冲的传送带":
buf数组:如果 channel 有缓冲,它就是传送带本体(满了/空了就会影响发送/接收能不能立刻完成)。- 读写位置:通过
sendx/recvx这些指针在传送带上"循环走"(走到头会绕回去)。 lock:每次操作通道内部状态(比如改指针、改计数)前先加锁,防止并发把结构弄乱。- 等待队列:当"送不进去/拿不到货"时,就把等着的人挂到
sendq/recvq里;每个等待者用sudog这个小包表示。 closed:通道有没有被关闭(关闭后发送会 panic,接收则通过ok=false告诉你没数据了)。
发送(ch <- x)
值要么立刻送到"正在等接收的人手里",要么塞进"缓冲区的传送带",不行就只能等到有机会了。
- 先判断通道是否已经关闭:如果
ch已关闭,你继续发送就会立刻 panic。 - 通道没关之后,再看有没有接收者在门口等(等待队列
recvq):- 有:直接把值交给接收者(跳过
buf),并唤醒对方。 - 没有:再看缓冲区有没有空位(没满的话就能塞进去)。
- 有:直接把值交给接收者(跳过
- 最后才区分"非阻塞"还是"阻塞":
- 非阻塞(
select + default):如果此刻明显发不出去(例如通道满了、或无法立刻完成),就直接返回"失败",让select走default,不会把当前 goroutine 挂起、也尽量不去抢锁。 - 阻塞:如果你允许等(没有
default兜底),而且确实发不出去,就把当前 goroutine 打包成sudog,挂到sendq上,然后gopark睡觉;等接收者来取货,再被唤醒继续把值送出去。
- 非阻塞(
go
// 非阻塞:如果发送此刻做不到,就直接走 default(不会卡住)
select {
case ch <- 1:
// 发送成功(立刻完成)
default:
// 发送此刻不行:走 default,不会阻塞
}

接收(x := <-ch)
值要么来自"正在等着发送的人",要么来自"缓冲区的传送带";如果两边都没有货,就看你是否允许阻塞。
- 先看有没有等待发送者(队列
sendq):- 有:直接从发送者手里拿值(跳过
buf),然后唤醒发送者。 - 没有:再看缓冲区有没有数据(
buf里是否有货)。
- 有:直接从发送者手里拿值(跳过
- 有货:从
buf头部取出,并更新读指针。 - 没货:
- 非阻塞(
select + default):立刻返回"接不到",让select走default。 - 阻塞:把当前 goroutine 打包成
sudog,挂进recvq,然后gopark睡觉;等有发送者来送值,或通道关闭后再被唤醒。
- 非阻塞(
接收不会 panic。你写 v, ok := <-ch 时:
ok == true:确实收到了值ok == false:通道已关闭,并且此刻没有更多数据可读了(缓冲里有数据会先读完,读完后才ok=false)
go
v, ok := <-ch
if !ok {
// 通道已关闭,且没有更多数据可读了
}
_ = v

阻塞 / panic 的典型情况(快速判定)
- 阻塞
- 在 nil channel 上发送和接收,并且没有select+default,就会进入阻塞流程:
- 发送:源码会先确认通道没关闭,然后如果门口没有等待接收者(
recvq没人),同时缓冲条件也不满足(无缓冲等价于"没人接收",有缓冲则是buf满了),就阻塞。 - 接收:同理,如果此刻拿不到(门口没有等待发送者:
sendq为空,且缓冲里也没有数据:buf为空),就阻塞
- panic
- panic 主要发生在"向已关闭通道发送"的场景里:当你执行
ch <- x时,源码会在加锁后检查c.closed != 0,如果发现通道已经关闭,就直接panic("send on closed channel"),不会走等待队列。 - 关闭未初始化的channel,关闭已关闭的通道也会panic
- panic 主要发生在"向已关闭通道发送"的场景里:当你执行
