第三章: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,导致数据竞争。从那以后,我写函数签名时都会带上方向。

小技巧: 在函数参数中明确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还没启动。

注意: 向一个nil的Channel发送或接收,会永远阻塞!这是Go语言里一个常见的坑。

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)
}
核心理解: 无缓冲Channel用于同步,有缓冲Channel用于异步。选哪个,取决于你的业务场景。

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是否关闭,有两种方式:

  1. 使用range循环:自动在Channel关闭且无数据时退出
  2. 使用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,接收方只管读
  • rangeok模式判断Channel是否关闭

下一章我们会深入Channel的高级用法,包括select多路复用和超时控制。到时候见!