1、通道的定义
通道的定义有3种方式:
1.1 声明但未初始化
go
//var ch chan T
//定义一个水管
var waterCh chan string
1.2 无缓冲通道定义:用make()创建
go
//ch := make(chan T)
//创建一个水管
waterCh := make(chan string)
无缓冲通道的特定:
- 写(ch <- v)会阻塞直到有goroutine读(<-ch)
- 读也会阻塞,直到有写发生
- 等价于"即时传输",非常适合用于同步,就像握手一样。
1.3 定义带缓冲的通道
go
//ch := make(chan T, N)
// T 通道类型, N 缓冲区长度
//创建一个带缓冲的水箱
waterCh := make(chan string,10)
有缓冲通道的特定:
- 写者在缓冲未满时不会阻塞(写入缓冲区),只有当缓冲区满了才阻塞;读在缓冲为空时阻塞
- 可以用来"解耦"生成/消费速率,但缓冲不是无限的
2、通道的基本操作(发送、接收、关闭)
go
package main
import (
"fmt"
"time"
)
func main() {
// 无缓冲示例:同步行为
unbuf := make(chan int)
go func() {
fmt.Println("goroutine: 准备发送 1(会等待接收)")
unbuf <- 1 // 阻塞,直到 main 接收
fmt.Println("goroutine: 发送完成")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("main: 开始接收")
fmt.Println("接收到:", <-unbuf)
// 有缓冲示例:解耦
buf := make(chan int, 2)
buf <- 10 // 不阻塞(缓冲未满)
buf <- 20 // 仍不阻塞
// buf <- 30 // 如果再写,会阻塞直到有人读
fmt.Println("从缓冲接收:", <-buf, <-buf)
// 关闭 channel(只能由发送方关闭)
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch) // 之后不能再发送,会 panic
for v := range ch { // range 会在 channel 被 close 且数据读尽后结束
fmt.Println("range got:", v)
}
time.Sleep(time.Second * 5)
}
注意:
- close(ch)的意思是"再也没有新的值会被发送到这个通道"
- 只有发送方应该关闭通道,接收方关闭会引发panic
- 向已经关闭的通道发送会导致panic;从已关闭通道接收会立即得到0值 使用comma-ok判断通道是否已关闭:
go
v, ok := <-ch
if !ok {
// ch 已经关闭且已经读完了
}
3、通道与同步
无缓冲通道本身就是最简单的同步原语:写者等待读者,读者等待写者->达成同步点,就像握手一样。
使用空结构体struct{}做信号:done:= make(chan struct{}),发送 done <- struct{}{}表示"完成"通知
也可以把sync.WaitGroup与通道混合使用。
例如:用通道做一次性完成通知
go
done := make(chan struct{})
go func() {
// 工作...
close(done) // 通知完成(常见做法)
}()
<-done // 等待完成
4、select:多路复用与超时
select类似switch,但只针对通道的发送接收:
go
select {
case v := <-ch1:
fmt.Println("从 ch1 收到", v)
case ch2 <- x:
fmt.Println("向 ch2 发送成功")
case <-time.After(time.Second * 2):
fmt.Println("超时了")
default:
fmt.Println("没有准备好的 case,立即执行 default(非阻塞)")
}
用time.After或者time.Timer可以实现超时或限时等待。select是实现非阻塞操作、超时、同时从多个通道接收的关键结构快。
5、常见模式
5.1 流水线:生产者->第一道工序->第二道工序->...->消费者,利用通道串联处理步骤,易于并行化和组合。
以汽车生产流水线(生产车身->安装发动机->安装轮子->下线)为例:
go
package main
import (
"log"
)
// 1. 生产车身
func createCarBody(carModels ...string) <-chan string {
bodyCh := make(chan string)
go func() {
defer close(bodyCh) // 生产完成后关闭传送带
for _, model := range carModels {
log.Printf("第一步:制造 %s 车身\n", model)
bodyCh <- model // 将车身送到车身传送带上
}
}()
return bodyCh
}
// 2. 安装发动机
func installEngine(bodyCh <-chan string) <-chan string {
withEnginCh := make(chan string)
go func() {
defer close(withEnginCh)
for car := range bodyCh { // 从车身传送带接收车身
log.Printf("第二步:为 %s 安装发动机\n", car)
withEnginCh <- car // 送往下一个传送带
}
}()
return withEnginCh
}
// 3. 安装轮胎
func installWheels(withEnginCh <-chan string) <-chan string {
withWheelsCh := make(chan string)
go func() {
defer close(withWheelsCh)
for car := range withEnginCh {
log.Printf("第三步:为 %s 安装轮胎\n", car)
withWheelsCh <- car
}
}()
return withWheelsCh
}
func main() {
// 设置生产线
bodyCh := createCarBody("SUV", "轿车", "跑车", "卡车")
withEngineCh := installEngine(bodyCh)
withWheelsCh := installWheels(withEngineCh)
// 最终质检并出厂
for car := range withWheelsCh {
log.Printf("第四步:完成生产,%s 已出厂\n", car)
}
}

5.2 扇出/扇入:多个worker从同一个jobs通道拉任务(扇出),他们把结果写到results通道(扇入)。
未来提高安装轮胎的效率,增加一个安装轮胎的工位:
go
// 合并多个通道的汽车(Fan-In)
func merge(cs ...<-chan string) <-chan string {
// 创建一个wg等待组
var wg sync.WaitGroup
out := make(chan string)
// 从每个输入通道收集数据
collect := func(in <-chan string) {
//协程函数完成,即wg计数减一
defer wg.Done()
for car := range in {
out <- car
}
}
// wg.Add(len(cs))
for _, c := range cs {
//wg计数加一
wg.Add(1)
go collect(c)
}
// 所有输入完成后关闭输出通道
go func() {
//等待所有的协程都执行完了才放过
wg.Wait()
close(out)
}()
return out
}
func main() {
carBodies := createCarBody("SUV", "轿车", "跑车", "卡车")
withEngines := installEngine(carBodies)
// Fan-Out: 创建2个轮胎安装工位并行工作
withWheelsCh1 := installWheels(withEngines) // 工位1
withWheelsCh2 := installWheels(withEngines) // 工位2
// Fan-In: 合并两个工位的输出
finishedCars := merge(withWheelsCh1, withWheelsCh2)
for car := range finishedCars {
log.Printf("第四步:完成生产, %s\n", car)
}
}

优雅退出:
实际生产中可能需要处理中断,例如紧急停机:
go
func installEngine(done <-chan struct{}, in <-chan string) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for car := range in {
select {
case out <- car:
fmt.Printf("为 %s 安装发动机\n", car)
case <-done: // 收到停机信号
fmt.Println("发动机安装工位紧急停机")
return
}
}
}()
return out
}
5.3 工作池:
模拟3个工人要完成5条任务
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 1. 创建两个通道
jobs := make(chan int, 5) // 任务队列,最多放5个任务
done := make(chan bool) // 用于通知所有任务完成的信号
var wg sync.WaitGroup // 用于等待所有工作者完成
// 2. 启动3个工作者 (Worker)
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// 3. 向任务队列添加5个任务
for j := 1; j <= 5; j++ {
jobs <- j // 发送任务编号
}
close(jobs) // 重要:发送完所有任务后关闭通道
// 4. 等待所有工作者完成任务
go func() {
wg.Wait() // 阻塞直到所有工作者都结束
close(done) // 发送完成信号
}()
// 5. 主程序在此等待,直到收到完成信号
<-done
fmt.Println("所有任务完成!")
}
// 工作者函数:从jobs通道接收任务并处理
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done() // 函数退出时通知WaitGroup此工作者已完成
for j := range jobs { // 循环从jobs通道读取任务,直到通道关闭
fmt.Printf("工作者%d 开始处理任务%d\n", id, j)
time.Sleep(time.Second) // 模拟任务耗时
fmt.Printf("工作者%d 完成处理任务%d\n", id, j)
}
}
通道在协程里的作用,就像人与人之间的沟通:
有时候我们需要面对面沟通(无缓冲通道);有时候需要有空的时候才回微信消息(有缓冲通道);select语句就像你同时处理多条消息,决定先回复哪一条或者不回复。学会恰当地选用同步或者缓冲,设置超时和优雅退出,代码会更健壮,人与人之间也是如此:及时、明确、还有体贴,是减少误解的关键。