Go Channel:通信实现共享内存

Channel 是 Go 语言并发编程的基石之一。它不仅是 goroutine 之间通信的管道,更承载了同步与数据传递的双重职责。
本文将系统梳理 Channel 的创建、读写、关闭的各种用法与注意事项,助你写出既优雅又健壮的并发代码。

1. 构造器函数:创建 Channel

Go 使用内置的 make 函数创建 Channel,可指定是否带缓冲区。

// 无缓冲通道
chUnbuf := make(chan int)

// 有缓冲通道,容量为 3
chBuf := make(chan int, 3)
类型 特点
无缓冲 同步通信:发送方阻塞直到接收方准备好
有缓冲 异步通信:缓冲区满前不阻塞发送,空时接收阻塞

最佳实践:优先使用无缓冲通道,除非你有充分的理由需要缓冲区(如平滑流量峰值)。

2. 从 Channel 读取数据

读操作有丰富的变体,适应不同场景。

2.1 正常读取(标准写法)

data := <-ch          // 读取并丢弃 ok 值
data, ok := <-ch // ok 表示通道是否仍然打开

2.2 忽略读取(扔掉数据)

<-ch                  // 仅用于同步(等待发信)

常用于信号量事件通知

done := make(chan struct{})
go func() {
doWork()
done <- struct{}{} // 发送空结构体
}()
<-done // 等待 goroutine 完成

2.3 判断读(检测通道关闭)

value, ok := <-ch
if !ok {
fmt.Println("通道已关闭,value 为零值")
} else {
fmt.Println("读到:", value)
}

2.4 阻塞读的三种情况

场景 行为
无缓冲通道,无发送方就绪 当前 goroutine 永久阻塞,直到有发送者
有缓冲通道,缓冲区为空 当前 goroutine 永久阻塞,直到有数据写入
nil channel 读取 永久阻塞(无法唤醒,属于程序 bug)
var nilCh chan int
<-nilCh // fatal error: all goroutines are asleep - deadlock!

2.5 优雅关闭读取:for range

for range 会持续从通道读取值,直到通道被关闭且缓冲区为空。

ch := make(chan int, 5)
// ... 写入数据后 close(ch)
for v := range ch {
fmt.Println(v)
}
// 等价于:
for {
v, ok := <-ch
if !ok {
break
}
fmt.Println(v)
}

2.6 用 select 实现非阻塞读

select {
case v, ok := <-ch:
if ok {
fmt.Println("读到:", v)
} else {
fmt.Println("通道已关闭")
}
default:
fmt.Println("通道暂时无数据,不阻塞")
}

3. 向 Channel 写入数据

写操作相对单纯,但某些错误会直接导致 panic。

3.1 正常写入

ch <- 42

3.2 阻塞写的三种情况

场景 行为
无缓冲通道,无接收方就绪 当前 goroutine 阻塞,直到有接收者
有缓冲通道,缓冲区已满 当前 goroutine 阻塞,直到有接收者腾出空间
nil channel 写入 永久阻塞
var nilCh chan int
nilCh <- 1 // deadlock!

3.3 写已关闭的通道 → panic

ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

这是 Go 运行时最有名的“礼节”之一:发送者负责关闭通道,接收者从不关闭

4. 关闭 Channel

关闭通道是一种广播行为,常用于通知接收者“没有更多数据了”。

4.1 正确关闭语法

close(ch)

4.2 关闭的三大原则

  1. 由发送方关闭,绝不是接收方。
  2. 关闭已经关闭的通道 → panic
  3. 关闭 nil 通道 → panic
ch := make(chan int)
close(ch) // OK
close(ch) // panic: close of closed channel

var nilCh chan int
close(nilCh) // panic: close of nil channel

4.3 关闭后接收方的行为

  • 通道关闭后,已缓冲的数据仍然可以被正常读取(按 FIFO 顺序)。
  • 缓冲区清空后,再读取会立即返回零值 + ok=false
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 0 (零值), 且 ok 为 false

4.4 安全关闭模式:使用 sync.Once

为避免多次关闭导致 panic,可以用 sync.Once

var once sync.Once
once.Do(func() { close(ch) })

5. 综合示例:生产者-消费者模型

func producer(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("produced: %d\n", i)
}
close(ch) // 生产者负责关闭
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for v := range ch { // 优雅读取,通道关闭后自动退出
fmt.Printf("consumed: %d\n", v)
}
}

func main() {
ch := make(chan int, 2)
var wg sync.WaitGroup
wg.Add(2)
go producer(ch, &wg)
go consumer(ch, &wg)
wg.Wait()
}

6. 常见陷阱与总结

操作 未关闭通道 已关闭通道 nil 通道
(<-ch) 阻塞或正常读取 返回零值 + ok=false 永久阻塞
(ch<-) 阻塞或正常写入 panic 永久阻塞
(close) 正常关闭 panic panic

最佳实践清单

  • ✅ 使用 for range 消费通道,自动处理关闭。
  • ✅ 发送方关闭通道,接收方绝不关闭。
  • ✅ 用 ok 模式判断通道是否关闭。
  • ✅ 使用 select + default 实现非阻塞 I/O。
  • ✅ 用 sync.Once 保护可能重复关闭的通道。
  • ❌ 不要向已关闭的 channel 发送数据。
  • ❌ 不要关闭一个其他 goroutine 还在写入的 channel(导致 panic)。
  • ❌ 不要依赖 len(ch) 来决定读写逻辑(竞争风险)。

掌握 Channel 的用法,你就能写出既清晰又安全的并发 Go 程序。
记住一句 Go 箴言:“Don’t communicate by sharing memory; share memory by communicating.” —— 而这正是 Channel 存在的意义。