Channel与单向Channel
2025/12/12大约 5 分钟
本篇是你 Go 并发学习的 第 9 天:专注 Channel 通信,结合你的环境给出可直接实操的练习示例。
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录示例:
/home/liumangmang/GolandProjects/go-channel-practice
📌 标题
Go 并发进阶:Channel(无缓冲/有缓冲)与单向 Channel 实战
✅ 步骤 1:创建练习项目
在终端中执行:
cd /home/liumangmang/GolandProjects
mkdir go-channel-practice && cd go-channel-practice
go mod init go-channel-practice✅ 步骤 2:无缓冲 channel 基础示例
创建 unbuffered.go:
nano unbuffered.go粘贴以下代码,体验 无缓冲 channel 的同步特性:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string) // 无缓冲 channel
go func() {
fmt.Println("[Sender] 准备发送数据...")
ch <- "hello from goroutine" // 这里会阻塞,直到有人接收
fmt.Println("[Sender] 数据发送完毕")
}()
time.Sleep(1 * time.Second)
fmt.Println("[Main] 1 秒后开始接收数据...")
msg := <-ch // 接收数据,同时解除发送方阻塞
fmt.Println("[Main] 收到:", msg)
fmt.Println("[Main] 程序结束")
}运行 & 观察
go run unbuffered.go你大概率会看到类似输出(时间顺序很关键):
[Sender] 准备发送数据...
[Main] 1 秒后开始接收数据...
[Main] 收到: hello from goroutine
[Sender] 数据发送完毕
[Main] 程序结束关键理解:
- 无缓冲 channel:发送和接收必须“同时就位” 才能完成一次传输。
ch <- value会阻塞,直到有value := <-ch在等待。- 这一特性非常适合做 goroutine 间的同步。
✅ 步骤 3:有缓冲 channel 示例
创建 buffered.go:
nano buffered.go粘贴以下代码,体验 有缓冲 channel 的“容量”效果:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3) // 容量为 3 的有缓冲 channel
fmt.Println("[Main] 开始发送 3 个元素...")
ch <- 1
fmt.Println("[Main] 已发送 1")
ch <- 2
fmt.Println("[Main] 已发送 2")
ch <- 3
fmt.Println("[Main] 已发送 3 (已满)")
// 再发送一个会怎样?
go func() {
fmt.Println("[Sender] 尝试发送第 4 个元素(会阻塞,直到有接收者)...")
ch <- 4
fmt.Println("[Sender] 第 4 个元素发送成功")
}()
time.Sleep(1 * time.Second)
fmt.Println("[Main] 开始接收...")
for i := 0; i < 4; i++ {
v := <-ch
fmt.Println("[Main] 收到:", v)
}
fmt.Println("[Main] 程序结束")
}运行 & 观察
go run buffered.go核心对比:
- 有缓冲 channel 在 未填满之前,发送不会阻塞。
- 当缓冲区 满了之后,发送会阻塞,直到有接收方读取。
- 这样可以在 异步生产/消费 场景中,减少 goroutine 的等待时间。
✅ 步骤 4:无缓冲 vs 有缓冲 对比小结
| 特性 | 无缓冲 channel | 有缓冲 channel |
|---|---|---|
| 创建方式 | make(chan T) | make(chan T, N) |
| 是否有缓存 | 否 | 是(容量为 N) |
| 发送是否阻塞 | 一定阻塞,直到有人接收 | 未满时不阻塞,满了才阻塞 |
| 适用场景 | 强同步、事件通知 | 异步队列、生产者-消费者、缓冲数据 |
实践建议:
- 如果你想表达“发送和接收必须同步发生”,优先用 无缓冲 channel。
- 如果你想“暂存一些数据,解耦生产和消费速度”,考虑用 有缓冲 channel。
✅ 步骤 5:单向 channel(只发 / 只收)
单向 channel 不是新类型,而是对 函数参数 做的“能力限制”,用于表达更清晰的意图:
- 只发送:
chan<- T - 只接收:
<-chan T
创建 directional.go:
nano directional.go粘贴以下代码:
package main
import "fmt"
// 只负责发送数据
func producer(out chan<- int) {
for i := 1; i <= 5; i++ {
fmt.Println("[Producer] 发送:", i)
out <- i
}
fmt.Println("[Producer] 关闭 channel")
close(out) // 只有发送方才能关闭
}
// 只负责接收数据
func consumer(in <-chan int) {
for v := range in { // 直到 channel 被关闭
fmt.Println("[Consumer] 接收:", v)
}
fmt.Println("[Consumer] channel 已关闭,接收结束")
}
func main() {
ch := make(chan int)
go producer(ch) // ch 在这里被当作 只发送 channel 使用
consumer(ch) // ch 在这里被当作 只接收 channel 使用
}运行
go run directional.go你会看到生产者发送 1~5,消费者依次接收,并在 channel 关闭后退出循环。
单向 channel 的价值
- 约束函数的职责:
producer只能发送(写),不能接收(读)。consumer只能接收(读),不能发送(写)。
- 提高可读性:别人一看函数签名就知道它的用途。
- 减少误用:编译器会阻止错误使用(比如在只读 channel 上发送)。
✅ 步骤 6:综合小实验:有缓冲 + 单向 channel
创建 pipeline.go,实现一个简单“生产者 → 处理者 → 消费者”的流水线:
nano pipeline.go粘贴以下代码:
package main
import "fmt"
// 生产者:产生 1~5
func producer(out chan<- int) {
for i := 1; i <= 5; i++ {
fmt.Println("[Producer] 发送:", i)
out <- i
}
close(out)
}
// 处理者:把数字放大 10 倍
func multiplier(in <-chan int, out chan<- int) {
for v := range in {
fmt.Println("[Multiplier] 接收:", v)
out <- v * 10
}
close(out)
}
// 消费者:打印结果
func consumer(in <-chan int) {
for v := range in {
fmt.Println("[Consumer] 最终结果:", v)
}
}
func main() {
ch1 := make(chan int, 2) // 有缓冲,减轻 producer 阻塞
ch2 := make(chan int, 2)
go producer(ch1)
go multiplier(ch1, ch2)
consumer(ch2)
}运行:
go run pipeline.go这个例子同时用到了:
- 有缓冲 channel(
ch1,ch2) - 单向 channel 函数参数
range channel+close实现优雅退出
⚠️ 常见坑与排查方式
坑 1:fatal error: all goroutines are asleep - deadlock!
- 常见原因:
- 无缓冲 channel 上只有发送,没有接收(或反之)。
- 有缓冲 channel 被写满后,没有消费者读取。
- 排查建议:
- 检查每一个
ch <-是否对应至少一个接收方。 - 检查是否在合适的地方
close(channel)让range能够退出。
- 检查每一个
- 常见原因:
坑 2:错误关闭 channel
- 只有 发送方 应该关闭 channel。
- 不要在多个 goroutine 中同时
close同一个 channel。
坑 3:误用单向 channel
- 记住:单向 channel 多用在 函数参数,一般不会在变量定义时直接写成单向。
📚 今日小结与思考题
你已经掌握:
- 无缓冲 channel:发送接收必须配对,同步传递数据。
- 有缓冲 channel:可以暂存 N 个元素,适合异步场景。
- 单向 channel:通过类型约束函数职责,提升代码可读性与安全性。
思考 & 练习:
- 修改
buffered.go,把缓冲区从 3 改成 1、10,观察阻塞行为的变化。 - 在
pipeline.go中增加一个新的阶段,例如:过滤掉奇数或小于 30 的数字。 - 尝试使用
time.Sleep人为制造“生产速度远快于消费”的情况,观察有缓冲 channel 的效果。
- 修改
如果你愿意,下一天我们可以继续练 select 多路复用 + 超时控制 + context 取消,构建更真实的并发场景 🚀
