前言
本章来了解下 golang 中 select 和 channel 的使用。Channel 是 Go 并发模型的核心——goroutine 之间通过 channel 进行通信和同步;而 select 则是多路 channel 操作的调度器,类似于网络编程中的 select/epoll。
一、Channel 基础
1.1 创建与基本操作
1 2 3 4 5 6 7 8 9
| var ch1 chan int ch2 := make(chan int) ch3 := make(chan int, 10)
ch2 <- 42 v := <-ch2 v, ok := <-ch2
|
1.2 无缓冲 vs 有缓冲
无缓冲 channel(make(chan T)):发送方会阻塞直到接收方准备好,反之亦然。用于 goroutine 之间的同步握手:
1 2 3 4 5 6 7 8 9 10 11
| func main() { ch := make(chan string)
go func() { time.Sleep(1 * time.Second) ch <- "hello" }()
msg := <-ch fmt.Println(msg) }
|
1 2
| 发送方 ──[阻塞]──> │无缓冲channel│ <──[阻塞]── 接收方 (容量=0,必须双方就绪)
|
有缓冲 channel(make(chan T, N)):缓冲区未满时发送不阻塞,未空时接收不阻塞。用于异步解耦:
1 2 3 4 5 6 7 8 9 10 11
| ch := make(chan int, 3)
ch <- 1 ch <- 2 ch <- 3
fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)
|
1 2
| 发送方 ──[写入]──> │[1][2][3]│ <──[读取]── 接收方 容量=3,有空间就不阻塞
|
1.3 关闭 channel
1 2 3 4 5 6 7 8 9 10 11 12
| close(ch)
v, ok := <-ch if !ok { fmt.Println("channel 已关闭") }
for v := range ch { fmt.Println(v) }
|
注意:
- 向已关闭的 channel 发送会 panic
- 从已关闭的 channel 接收会立即返回零值(不会阻塞)
- 多次 close 会 panic
1.4 单向 channel(类型约束)
1 2 3 4 5 6 7 8
| func producer(ch chan<- int) { ... } func consumer(ch <-chan int) { ... }
var ch = make(chan int) producer(ch) consumer(ch)
|
单向 channel 主要用于接口约束,明确表达函数对 channel 的使用意图。
二、Select 机制
2.1 基本语法
select 用于同时等待多个 channel 操作,哪个先就绪执行哪个。如果多个同时就绪,随机选一个:
1 2 3 4 5 6 7 8 9 10
| select { case v := <-ch1: fmt.Println("从 ch1 收到:", v) case v := <-ch2: fmt.Println("从 ch2 收到:", v) case ch3 <- 42: fmt.Println("发送成功") default: fmt.Println("没有 channel 就绪") }
|
2.2 典型模式
模式一:超时控制
1 2 3 4 5 6
| select { case result := <-doWork(): handle(result) case <-time.After(3 * time.Second): fmt.Println("超时了!") }
|
模式二:退出信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func worker(done chan struct{}) { ticker := time.NewTicker(500 * time.Millisecond)
for { select { case <-ticker.C: fmt.Println("工作中...") case <-done: fmt.Println("收到退出信号") return } } }
done := make(chan struct{}) go worker(done) time.Sleep(2 * time.Second) close(done)
|
模式三:多路合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func merge(cs ...<-chan int) <-chan int { out := make(chan int)
var wg sync.WaitGroup for _, c := range cs { wg.Add(1) go func(ch <-chan int) { defer wg.Done() for v := range ch { out <- v } }(c) }
go func() { wg.Wait() close(out) }() return out }
|
2.3 空选择 — 永久阻塞
这等价于 for {} 或 <-make(chan struct{}),但语义更清晰——明确表示”我在等待某个事件”。
三、常见陷阱与注意事项
陷阱 1:向 nil channel 发送/接收永远阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| var ch chan int
select { case ch <- 1: default: fmt.Println("进入 default") }
func process(input <-chan int, stop <-chan struct{}) { var in <-chan int
for { select { case <-stop: return case v, ok := <-in: if !ok { in = nil continue } fmt.Println(v) }
if in == nil && input != nil { in = input } } }
|
这是利用 nil channel 做动态启停的常用技巧。
陷阱 2:select 的 case 分支是随机选择
当多个 channel 同时就绪时,Go 运行时随机选择一个 case 执行(不是按代码顺序):
1 2 3 4 5 6 7 8 9 10 11 12
| ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch1 <- 1 ch2 <- 2
select { case <-ch1: fmt.Println("ch1") case <-ch2: fmt.Println("ch2") }
|
这保证了公平性——避免某个 channel 总是被优先选中而饿死其他 channel。
陷阱 3:for-select 中忘记 break
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| for { select { case <-quit: break } }
loop: for { select { case <-quit: break loop } }
|
陷阱 4:buffer 大小设置不当导致死锁
1 2 3 4 5 6 7
| ch := make(chan int, 1) ch <- 1 ch <- 2
go func() { ch <- 2 }()
|
四、实战示例:带超时的任务分发器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| type Task struct{ ID int }
func runTasks(tasks []Task, timeout time.Duration) []Task { results := make([]Task, 0, len(tasks)) taskCh := make(chan Task, len(tasks)) doneCh := make(chan struct{})
const workers = 4 for i := 0; i < workers; i++ { go func(id int) { for t := range taskCh { time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) results = append(results, t) fmt.Printf("[worker-%d] 完成 task-%d\n", id, t.ID) } }(i) }
go func() { for _, t := range tasks { taskCh <- t } close(taskCh) }
timer := time.NewTimer(timeout) defer timer.Stop()
select { case <-doneCh: fmt.Println("全部完成") case <-timer.C: fmt.Printf("超时 %s,已完成 %d/%d\n", timeout, len(results), len(tasks)) }
return results }
|
五、总结
| 特性 |
Channel |
Select |
| 核心作用 |
goroutine 间通信 |
多路 channel 调度 |
| 同步机制 |
无缓冲 = 同步握手 |
阻塞等待任一就绪 |
| 关键点 |
方向性、关闭、nil channel |
随机公平、default 分支 |
| 典型用途 |
数据传递、信号通知 |
超时控制、退出监听 |
Go 并发哲学:Don’t communicate by sharing memory; instead, share memory by communicating.