第 21 课:Channel 通道
学习目标
完成本课程后,你将能够:
- 理解 Channel 在 Go 并发中的核心作用——安全的通信。
- 掌握创建、发送(send)和接收(receive) Channel 数据的基本操作。
- 理解无缓冲(unbuffered)和有缓冲(buffered) Channel 的行为差异及其导致的阻塞特性。
- 了解双向和单向(方向限制) Channel 的使用场景。
- 能够编写使用 Channel 进行 goroutine 间通信的简单程序。
核心概念
1. 什么是 Channel?
想象一下两个人需要传递物品。他们可以通过一个管道(pipe)来传递,一人从一端放入,另一人从另一端取出。在 Go 中,Channel 就是这样的“管道”,用于在并发的 goroutine 之间传递数据。它是 Go 并发哲学“不要通过共享内存来通信,而要通过通信来共享内存”的核心体现。
Channel 是类型相关的,例如 chan int 表示一个只能传递 int 类型数据的 Channel。
2. 基本操作:发送与接收
- 创建:使用
make函数。ch := make(chan int)创建一个无缓冲的 int 类型 Channel。 - 发送:使用
<-操作符,箭头方向指向 Channel。ch <- value将value发送到ch。 - 接收:同样使用
<-操作符,箭头方向从 Channel 指出。value := <-ch从ch接收数据并赋值给value。
关键特性:发送和接收操作都会导致 goroutine 阻塞,直到另一端准备好。
- 对于无缓冲 Channel:发送方会阻塞,直到有接收方准备好接收;接收方会阻塞,直到有发送方发送数据。它强制进行同步,像两人直接握手交接物品。
- 对于有缓冲 Channel:创建时指定容量,如
ch := make(chan int, 5)。发送方只在缓冲区满时阻塞;接收方只在缓冲区空时阻塞。它像一个有容量的队列。
3. 关闭 Channel
发送方可以使用 close(ch) 来关闭 Channel,表示不再有数据发送。接收方可以通过第二个返回值来判断 Channel 是否已关闭:
val, ok := <-ch // ok 为 false 时,表示 Channel 已关闭且没有值可接收
重要:永远不要关闭一个接收方的 Channel,通常由发送方在确认不再发送数据后关闭。
代码示例
示例1:基础的无缓冲 Channel 通信
package main
import (
"fmt"
"time"
)
// 生产者:向 channel 发送数据
func producer(ch chan<- int) {
fmt.Println("生产者准备发送数据...")
ch <- 100 // 发送数据,会阻塞,直到消费者接收
fmt.Println("生产者发送完毕")
}
// 消费者:从 channel 接收数据
func consumer(ch <-chan int) {
fmt.Println("消费者准备接收数据...")
val := <-ch // 接收数据,会阻塞,直到生产者发送
fmt.Println("消费者收到数据:", val)
}
func main() {
// 创建一个无缓冲的 int channel
ch := make(chan int)
go producer(ch) // 启动生产者 goroutine
go consumer(ch) // 启动消费者 goroutine
// 等待一段时间,让 goroutine 有机会执行,实际应用中会用更优雅的方式同步
time.Sleep(time.Second)
fmt.Println("主函数结束")
}
示例2:有缓冲 Channel 示例
package main
import "fmt"
func main() {
// 创建一个容量为 2 的缓冲 channel
ch := make(chan string, 2)
// 连续发送两次,不会立即阻塞(因为缓冲区有空间)
ch <- "Hello"
ch <- "Go"
// 连续接收两次
fmt.Println(<-ch) // 输出: Hello
fmt.Println(<-ch) // 输出: Go
// 如果尝试发送第三次,此时缓冲区已满,会导致死锁(主goroutine阻塞)
// ch <- "World" // 取消注释将导致死锁
}
示例3:方向限制 Channel
package main
import "fmt"
// 此函数只能向 channel 发送数据
func sendOnly(ch chan<- int, val int) {
ch <- val
}
// 此函数只能从 channel 接收数据
func receiveOnly(ch <-chan int) int {
return <-ch
}
func main() {
// 一个双向 channel
biCh := make(chan int)
// 将双向 channel 传递给需要方向限制的函数
go sendOnly(biCh, 42)
result := receiveOnly(biCh)
fmt.Println("结果:", result) // 输出: 结果: 42
}
实践练习
练习1:基础数据交换
编写两个 goroutine,一个发送一个字符串 "ping" 到 Channel,另一个接收并打印它。确保程序不会因 goroutine 提前退出而结束。
预期输出:ping
练习2:探索缓冲 Channel
- 创建一个容量为 3 的
chan bool。 - 启动一个 goroutine 向其中发送 3 次
true,每次发送后打印"Sent"。 - 在主 goroutine 中,等待 1 秒后,连续接收 3 次并打印接收到的值。
- 思考:为什么所有
"Sent"会在接收开始前打印出来? 预期输出(顺序):
Sent
Sent
Sent
true
true
true
练习3:生产者-消费者模型
模拟一个简单的生产者-消费者模型:
- 生产者 goroutine 生成数字 1 到 5,并发送到 Channel。
- 消费者 goroutine 从 Channel 接收这些数字并计算它们的总和。
- 主 goroutine 等待消费者完成,并输出总和。
提示:生产者发送完毕后需要关闭 Channel,消费者需要通过循环或
range来接收所有数据。 预期输出:总和: 15
常见错误
-
死锁(Deadlock):最常见的错误。当所有 goroutine 都在等待,没有一个能继续执行时发生。
- 场景:只在一个 goroutine 中对一个无缓冲 Channel 进行发送或接收。
- 示例:
ch := make(chan int); ch <- 1 // 主 goroutine 阻塞,没有其他 goroutine 来接收 - 解决:确保有匹配的发送和接收方在并发运行,或使用有缓冲 Channel 并确保其容量足够。
-
忘记关闭 Channel:导致接收方的
range循环永远阻塞。- 场景:使用
for val := range ch循环接收数据,但发送方忘记在发送完所有数据后调用close(ch)。 - 解决:在发送方确认不再发送后,显式关闭 Channel。
- 场景:使用
-
误解阻塞时机:认为有缓冲 Channel 永远不会阻塞。
- 解决:牢记:缓冲满时发送阻塞,缓冲空时接收阻塞。
-
在接收方关闭 Channel:这可能导致“send on closed channel” panic。
- 解决:明确所有权,通常由发送数据的一方负责关闭 Channel。
-
忽略接收零值:当 Channel 已关闭且没有值时,接收操作会立即返回该类型的零值(如
int的0,string的"")。- 解决:始终使用
val, ok := <-ch来检查是否成功接收。
- 解决:始终使用
小结
- Channel 是 Go 中用于 goroutine 间通信的并发安全的数据管道。
- 操作:使用
<-进行发送(ch <- val)和接收(val <- ch)。创建用make(chan Type)或make(chan Type, Capacity)。 - 阻塞规则:这是 Channel 的核心行为。无缓冲 Channel 要求收发同步;有缓冲 Channel 在缓冲区满/空时阻塞。
- 关闭:发送方使用
close(ch)关闭 Channel,表明数据发送结束。接收方通过val, ok := <-ch的ok值判断。 - 方向限制:
chan<-只发送,<-chan只接收,用于函数参数类型声明,增强程序安全性和可读性。
理解并善用 Channel 的阻塞特性,是编写正确、高效并发程序的关键。在下一课中,我们将学习由这些基础特性衍生出的常见 Channel 模式。