第三章:Channel基础——Go并发编程的“血管”
如果说Goroutine是Go语言并发模型的心脏,那Channel就是连接心脏的血管。没有Channel,Goroutine之间就是一座座孤岛。今天我们就来彻底搞懂Channel的创建、使用、缓冲机制以及关闭的注意事项。
3.1 Channel的创建与类型
Channel说白了就是一个管道,用来在Goroutine之间传递数据。它的类型由它传递的数据类型决定。
创建一个Channel很简单,用make函数就行:
// 创建一个传递int类型的无缓冲Channel
ch := make(chan int)
// 创建一个传递string类型,容量为3的有缓冲Channel
chStr := make(chan string, 3)
我个人习惯在声明Channel时,明确指定它是只读还是只写的。这能避免很多运行时错误。比如:
// 只读Channel
func readOnly(ch <-chan int) {
val := <-ch
fmt.Println(val)
}
// 只写Channel
func writeOnly(ch chan<- int) {
ch <- 42
}
你想想看,如果一个函数只负责往Channel里写数据,那它就不应该能读。反过来也一样。这种约束在大型项目中特别有用,我在重构一个消息队列中间件时就吃过这个亏——一个Goroutine不小心读了不该读的Channel,导致数据竞争。从那以后,我写函数签名时都会带上方向。
3.2 Channel的发送与接收操作
发送和接收是Channel最核心的操作。语法非常直观:
ch := make(chan int)
// 发送:将值放入Channel
go func() {
ch <- 100
}()
// 接收:从Channel取出值
val := <-ch
fmt.Println(val) // 输出 100
这里有个关键点:发送和接收是阻塞的。什么意思?
- 发送操作会阻塞,直到有另一个Goroutine接收
- 接收操作会阻塞,直到有另一个Goroutine发送
嗯,这就是无缓冲Channel的同步特性。我曾经在写一个日志收集系统时,因为没理解这个阻塞特性,导致主Goroutine一直卡在发送操作上,整个程序看起来像死锁了一样。排查了半天才发现是接收的Goroutine还没启动。
3.3 无缓冲Channel与有缓冲Channel
这是Channel最核心的区别,也是面试常问的点。
| 特性 | 无缓冲Channel | 有缓冲Channel |
|---|---|---|
| 创建方式 | make(chan T) |
make(chan T, capacity) |
| 发送行为 | 必须有接收者,否则阻塞 | 缓冲区未满时立即返回,满时阻塞 |
| 接收行为 | 必须有发送者,否则阻塞 | 缓冲区非空时立即返回,空时阻塞 |
| 典型用途 | 同步、信号传递 | 解耦、流量控制 |
说白了,无缓冲Channel就是“一手交钱,一手交货”。发送方和接收方必须同时在场。而有缓冲Channel就像个邮箱,你可以先把信放进去,收信人什么时候来取都行。
举个例子:
// 无缓冲:必须配对
ch := make(chan int)
go func() {
ch <- 1 // 阻塞,直到主Goroutine接收
}()
fmt.Println(<-ch) // 主Goroutine接收,解除阻塞
// 有缓冲:可以存着
bufCh := make(chan int, 2)
bufCh <- 1 // 不阻塞
bufCh <- 2 // 不阻塞
// bufCh <- 3 // 缓冲区满了,会阻塞
fmt.Println(<-bufCh) // 取出1
fmt.Println(<-bufCh) // 取出2
我在项目中遇到过这样一个场景:需要限制并发请求的数量。用有缓冲Channel做信号量就特别合适:
// 限制最多10个并发
sem := make(chan struct{}, 10)
for _, task := range tasks {
sem <- struct{}{} // 获取信号量,满时阻塞
go func(t Task) {
defer func() { <-sem }() // 释放信号量
process(t)
}(task)
}
3.4 Channel的关闭与判断
Channel用完了要关吗?答案是:只有发送方才能关闭Channel。接收方永远不应该关闭Channel。
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch) // 发送方关闭
// 接收方判断Channel是否已关闭
for val := range ch {
fmt.Println(val) // 输出 1, 2
}
// 循环结束后,Channel已关闭且无数据
为什么只能发送方关闭?因为接收方不知道还有没有数据要发。如果接收方关了,发送方还在发,就会panic。
判断Channel是否关闭,有两种方式:
- 使用
range循环:自动在Channel关闭且无数据时退出 - 使用
ok模式:val, ok := <-ch,ok为false表示Channel已关闭
// ok模式示例
ch := make(chan int)
close(ch)
val, ok := <-ch
fmt.Println(val, ok) // 输出 0 false(int零值,Channel已关闭)
我曾经犯过一个错误:在多个Goroutine中同时关闭同一个Channel。结果程序直接panic了。记住:关闭Channel只能做一次,且只能由发送方做。
- 向已关闭的Channel发送数据 → panic
- 关闭已关闭的Channel → panic
- 从已关闭的Channel接收数据 → 安全,会返回零值
嗯,这里有个小技巧:如果你不确定Channel是否已关闭,可以用sync.Once来保证只关闭一次:
var closeOnce sync.Once
closeOnce.Do(func() {
close(ch)
})
总结一下今天的核心:
- Channel是类型安全的管道,用
make创建 - 无缓冲Channel同步,有缓冲Channel异步
- 发送方负责关闭Channel,接收方只管读
- 用
range或ok模式判断Channel是否关闭
下一章我们会深入Channel的高级用法,包括select多路复用和超时控制。到时候见!