Go: Go Select 与并发模式
select 是 Go 并发编程的瑞士军刀——同时监听多个 channel,谁先就绪处理谁。它是连接 goroutine 的"智能交换机"。
如果说 channel 是 goroutine 之间的电话线,select 就是接线员——它同时监听所有电话,哪个响就接哪个。这节课你将掌握 select 的全部核心模式。
1. 你将学到
- select 多路复用的语法与行为
- 随机选择(多个 case 同时就绪)
time.After超时控制default非阻塞操作for-select循环退出(done channel)- fan-out 模式分发任务
- fan-in 模式聚合结果
- pipeline 模式构建数据处理流水线
2. 一个量化交易工程师的真实故事
(1) 痛点:轮询 3 个数据源,CPU 跑满
Bob 是量化交易团队的后端工程师,他需要同时监听 3 个股票数据源:
"我们的策略需要同时从 NYSE、NASDAQ 和 LSE 三个交易所获取实时报价。之前的 Java 实现用一个线程轮询三个 WebSocket,每次 10ms 的 busy wait,CPU 占用 30%——老板说我比交易系统还费电。"
他打开现在的代码:
// 坏代码:忙轮询
func pollDataSources() {
for {
// 每隔 10ms 轮询一次,浪费 CPU
if data1 := pollNYSE(); data1 != nil {
process(data1)
}
if data2 := pollNASDAQ(); data2 != nil {
process(data2)
}
if data3 := pollLSE(); data3 != nil {
process(data3)
}
time.Sleep(10 * time.Millisecond)
}
}
三个问题:(1) 10ms 轮询间隔浪费 CPU;(2) 数据到达和处理不同步;(3) 轮询延迟 0~10ms 不可预测。
(2) Go 的解法:select 监听三个 channel
// market_data.go
package main
import (
"fmt"
"math/rand"
"time"
)
type Quote struct {
Source string
Symbol string
Price float64
}
func simulateExchange(name string, out chan<- Quote) {
symbols := []string{"AAPL", "GOOGL", "MSFT", "AMZN"}
for {
time.Sleep(time.Duration(50+rand.Intn(200)) * time.Millisecond)
quote := Quote{
Source: name,
Symbol: symbols[rand.Intn(len(symbols))],
Price: 100 + rand.Float64()*200,
}
out <- quote
}
}
func main() {
nyse := make(chan Quote)
nasdaq := make(chan Quote)
lse := make(chan Quote)
go simulateExchange("NYSE", nyse)
go simulateExchange("NASDAQ", nasdaq)
go simulateExchange("LSE", lse)
// select 同时监听三个 channel
timeout := time.After(2 * time.Second)
for {
select {
case q := <-nyse:
fmt.Printf("[NYSE] %s: $%.2f\n", q.Symbol, q.Price)
case q := <-nasdaq:
fmt.Printf("[NASDAQ] %s: $%.2f\n", q.Symbol, q.Price)
case q := <-lse:
fmt.Printf("[LSE] %s: $%.2f\n", q.Symbol, q.Price)
case <-timeout:
fmt.Println("演示结束")
return
}
}
}
输出:
[NASDAQ] AMZN: $198.32
[NYSE] AAPL: $150.45
[LSE] MSFT: $287.10
...
(3) 收益:select 监听 vs 轮询
| 方式 | CPU 占用 | 响应延迟 | 代码复杂度 |
|---|---|---|---|
| 轮询 10ms | 30% | 0~10ms | 低 |
| 轮询 100ms | 3% | 0~100ms | 低 |
| select 监听 | 0% | 0ms(实时) | 中 |
3. select 基础
(1) select syntax
select {
case v := <-ch1:
// ch1 就绪
case v := <-ch2:
// ch2 就绪
case ch3 <- value:
// 可以向 ch3 发送(ch3 有空间或有接收方)
default:
// 所有 channel 都未就绪
}
▶ 示例:select 随机选择
package main
import (
"fmt"
)
func main() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "来自 ch1"
ch2 <- "来自 ch2"
// 两个 channel 同时就绪,select 随机选一个
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
}
输出(每次可能不同):
来自 ch1
来自 ch2
4. time.After 超时控制
(1) 超时模式
package main
import (
"fmt"
"time"
)
func slowOperation() string {
time.Sleep(2 * time.Second)
return "结果"
}
func main() {
ch := make(chan string)
go func() {
ch <- slowOperation()
}()
select {
case result := <-ch:
fmt.Println("成功:", result)
case <-time.After(1 * time.Second):
fmt.Println("超时!操作超过 1 秒")
}
}
输出:
超时!操作超过 1 秒
▶ 示例:逐级超时(分级等待)
package main
import (
"fmt"
"time"
)
func fetchFromCache() string {
time.Sleep(50 * time.Millisecond)
return "cache-data"
}
func fetchFromDB() string {
time.Sleep(200 * time.Millisecond)
return "db-data"
}
func fetchFromAPI() string {
time.Sleep(500 * time.Millisecond)
return "api-data"
}
func main() {
cache := make(chan string, 1)
db := make(chan string, 1)
api := make(chan string, 1)
go func() { cache <- fetchFromCache() }()
go func() { db <- fetchFromDB() }()
go func() { api <- fetchFromAPI() }()
select {
case r := <-cache:
fmt.Println("缓存命中:", r)
case <-time.After(100 * time.Millisecond):
select {
case r := <-db:
fmt.Println("DB 返回:", r)
case <-time.After(300 * time.Millisecond):
select {
case r := <-api:
fmt.Println("API 返回:", r)
case <-time.After(600 * time.Millisecond):
fmt.Println("所有数据源超时!")
}
}
}
}
time.After(d) 返回 <-chan time.Time,在 d 时间后发送当前时间。每次 select 调用 time.After 都会创建一个新 timer——如果在循环中频繁调用,记得用 time.NewTimer 并 Stop,避免资源泄漏。
5. default 非阻塞操作
(1) 非阻塞发送/接收
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
// 非阻塞接收
select {
case v := <-ch:
fmt.Println("收到:", v)
default:
fmt.Println("无数据(非阻塞)")
}
// 非阻塞发送
ch <- 1
select {
case ch <- 2:
fmt.Println("发送成功")
default:
fmt.Println("缓冲已满(非阻塞)")
}
}
输出:
无数据(非阻塞)
缓冲已满(非阻塞)
▶ 示例:非阻塞 channel + 轮询熔断
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
go func() {
for i := 1; i <= 10; i++ {
select {
case ch <- i:
// 发送成功
default:
fmt.Printf("缓冲满,丢弃 %d\n", i)
}
time.Sleep(10 * time.Millisecond)
}
close(ch)
}()
// 慢消费者
for v := range ch {
fmt.Printf("处理: %d\n", v)
time.Sleep(50 * time.Millisecond)
}
}
输出:
处理: 1
处理: 2
处理: 3
缓冲满,丢弃 4
缓冲满,丢弃 5
处理: 6
...
default 会让 select 立即返回——如果所有 channel 都未就绪就执行 default。这非常适合做非阻塞的 channel 操作,但要小心在 for 循环中用 default 会造成 busy loop。
6. for-select 循环与 done channel
(1) done channel 退出模式
package main
import (
"fmt"
"time"
)
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("worker 退出")
return
default:
fmt.Println("worker 工作中...")
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(1 * time.Second)
close(done)
time.Sleep(100 * time.Millisecond)
fmt.Println("main 退出")
}
▶ 示例:for-select 三种退出方式
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
done := make(chan struct{})
// 生产者
go func() {
for i := 1; i <= 5; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
close(ch)
}()
// 消费者(for-select 循环)
go func() {
for {
select {
case v, ok := <-ch:
if !ok {
fmt.Println("方式1: channel 关闭退出")
close(done)
return
}
fmt.Printf("处理: %d\n", v)
case <-time.After(1 * time.Second):
fmt.Println("方式2: 超时退出")
close(done)
return
}
}
}()
<-done
fmt.Println("main 退出")
}
(2) for-select 退出模式对比
| 模式 | 触发条件 | 优点 | 缺点 |
|---|---|---|---|
| close(ch) | 发送方关闭 channel | 自然结束 | 只能用于接收方 |
| done channel | close(done) 信号 |
灵活,可由外部触发 | 需要额外 channel |
| timeout | time.After 超时 |
防卡死 | 硬超时不够灵活 |
| context | ctx.Done() |
可传递取消信号 | 第 17 课深入 |
7. fan-out / fan-in 模式
(1) fan-out:任务分发
package main
import (
"fmt"
"sync"
)
func fanOut(jobs <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
ch := make(chan int, 10)
channels[w] = ch
go func(id int, out chan<- int) {
defer wg.Done()
defer close(out)
for job := range jobs {
result := job * job
fmt.Printf("Worker %d: %d^2 = %d\n", id, job, result)
out <- result
}
}(w, ch)
}
return channels
}
▶ 示例:fan-in:结果聚合
package main
import (
"fmt"
"sync"
)
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
jobs := make(chan int, 10)
for i := 1; i <= 6; i++ {
jobs <- i
}
close(jobs)
// fan-out:分发给 3 个 worker
workers := fanOut(jobs, 3)
// fan-in:聚合所有 worker 的结果
results := fanIn(workers...)
// 收集结果
sum := 0
count := 0
for r := range results {
sum += r
count++
}
fmt.Printf("共 %d 个结果, 总和 = %d\n", count, sum)
}
输出:
Worker 2: 1^2 = 1
Worker 0: 3^2 = 9
Worker 1: 2^2 = 4
Worker 1: 5^2 = 25
Worker 0: 4^2 = 16
Worker 2: 6^2 = 36
共 6 个结果, 总和 = 91
(2) fan-out vs fan-in
| 模式 | 方向 | 用途 |
|---|---|---|
| fan-out | 1 个 channel → N 个 channel | 任务分发、并行计算 |
| fan-in | N 个 channel → 1 个 channel | 结果聚合、日志收集 |
flowchart LR
subgraph FanOut [Fan-Out]
J[Jobs Channel] --> W1[Worker 1]
J --> W2[Worker 2]
J --> W3[Worker 3]
end
subgraph FanIn [Fan-In]
W1 --> R[Results Channel]
W2 --> R
W3 --> R
end
style J fill:#e1f5fe
style R fill:#fff3e0
8. 完整示例:三数据源股票行情聚合
// market_aggregator.go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// ---------- 数据类型 ----------
type Trade struct {
Source string
Symbol string
Price float64
Volume int
Time time.Time
}
type AggregatedQuote struct {
Symbol string
AvgPrice float64
TotalVolume int
Sources int
High float64
Low float64
Time time.Time
}
// ---------- 模拟交易所数据源 ----------
var symbols = []string{"AAPL", "GOOGL", "MSFT", "AMZN", "TSLA", "META"}
func simulateExchange(name string, out chan<- Trade, done <-chan struct{}) {
for {
select {
case <-done:
return
default:
time.Sleep(time.Duration(100+rand.Intn(300)) * time.Millisecond)
trade := Trade{
Source: name,
Symbol: symbols[rand.Intn(len(symbols))],
Price: 100 + rand.Float64()*200,
Volume: rand.Intn(1000) + 100,
Time: time.Now(),
}
select {
case out <- trade:
case <-done:
return
}
}
}
}
// ---------- 聚合器 ----------
type Aggregator struct {
trades chan Trade
done chan struct{}
wg sync.WaitGroup
}
func NewAggregator() *Aggregator {
return &Aggregator{
trades: make(chan Trade, 100),
done: make(chan struct{}),
}
}
func (a *Aggregator) AddSource(name string) {
a.wg.Add(1)
go func() {
defer a.wg.Done()
simulateExchange(name, a.trades, a.done)
}()
fmt.Printf("添加数据源: %s\n", name)
}
func (a *Aggregator) Start(interval time.Duration, callback func(map[string]*AggregatedQuote)) {
// 聚合窗口
window := make(map[string][]Trade)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case trade := <-a.trades:
window[trade.Symbol] = append(window[trade.Symbol], trade)
case <-ticker.C:
// 时间窗口到,计算聚合报价
quotes := make(map[string]*AggregatedQuote)
for symbol, trades := range window {
if len(trades) == 0 {
continue
}
var sumPrice float64
var totalVol int
high := trades[0].Price
low := trades[0].Price
for _, t := range trades {
sumPrice += t.Price * float64(t.Volume)
totalVol += t.Volume
if t.Price > high {
high = t.Price
}
if t.Price < low {
low = t.Price
}
}
sources := make(map[string]bool)
for _, t := range trades {
sources[t.Source] = true
}
quotes[symbol] = &AggregatedQuote{
Symbol: symbol,
AvgPrice: sumPrice / float64(totalVol),
TotalVolume: totalVol,
Sources: len(sources),
High: high,
Low: low,
Time: time.Now(),
}
}
callback(quotes)
window = make(map[string][]Trade) // 重置窗口
case <-a.done:
return
}
}
}
func (a *Aggregator) Stop() {
close(a.done)
a.wg.Wait()
}
// ---------- 主函数 ----------
func main() {
aggregator := NewAggregator()
// 添加 3 个数据源
aggregator.AddSource("NYSE")
aggregator.AddSource("NASDAQ")
aggregator.AddSource("LSE")
fmt.Println("\n开始聚合(每 2 秒输出一次)...\n")
// 启动聚合,2 秒时间窗口
done := make(chan struct{})
go func() {
aggregator.Start(2*time.Second, func(quotes map[string]*AggregatedQuote) {
fmt.Printf("=== 聚合报告 %s ===\n", time.Now().Format("15:04:05"))
for _, q := range quotes {
fmt.Printf("%-6s | $%.2f (avg) | vol=%d | src=%d | H=%.2f L=%.2f\n",
q.Symbol, q.AvgPrice, q.TotalVolume, q.Sources, q.High, q.Low)
}
fmt.Println()
})
close(done)
}()
// 运行 10 秒后停止
time.Sleep(10 * time.Second)
aggregator.Stop()
<-done
fmt.Println("行情聚合器已停止")
}
预期输出:
添加数据源: NYSE
添加数据源: NASDAQ
添加数据源: LSE
开始聚合(每 2 秒输出一次)...
=== 聚合报告 10:00:02 ===
AAPL | $152.34 (avg) | vol=2341 | src=3 | H=165.20 L=142.10
GOOGL | $178.90 (avg) | vol=1567 | src=2 | H=185.00 L=172.30
MSFT | $295.40 (avg) | vol=890 | src=3 | H=301.20 L=288.50
=== 聚合报告 10:00:04 ===
...
行情聚合器已停止
case <-done: 和 case trade := <-a.trades: 需要同时监听。如果只监听 trades channel,Stop 信号就无法送达。任何 for-select 循环都必须包含退出条件。
❓ 常见问题
time.After(d) 返回一个 channel,在 d 时间后收到值。在 select 中用 case <-time.After(d): 作为超时分支。注意:每次 select 调用 time.After 都会创建新 timer,循环中建议用 time.NewTimer 复用。time.Sleep 或限制频率。done := make(chan struct{}),select 中监听 case <-done:,需要退出时 close(done)。所有监听 done 的 goroutine 会同时收到退出信号。close 比发送一个值更优——可被多次接收。case ch <- value: 也是合法的 select case。当 channel 有空间(有缓冲)或有接收方(无缓冲)时就绪。这个特性用于限流和信号量模式。select {} 会永久阻塞——因为没有任何 case,且没有 default。这可以作为"永久等待"的信号。但生产代码中应该确保有退出路径,否则 goroutine 泄漏。break label,或 return,或 done channel。📖 小节
- select 同时监听多个 channel,谁先就绪处理谁
- 多个 case 同时就绪时随机选择
time.After实现超时控制,time.NewTimer复用于循环default让 select 非阻塞,适合熔断/尝试操作done channel是 for-select 循环的标准退出方式- fan-out 分发任务,fan-in 聚合结果,两者组合成并行管道
- 每个 for-select 必须包含退出条件
📝 作业
-
基础题(难度⭐):写一个程序启动 3 个 goroutine,分别发送整数到各自的 channel,用 select 同时接收并打印。添加一个超时分支,3 秒后退出。
-
进阶题(难度⭐⭐):实现一个超时缓存:
func FetchWithCache(key string, cache map[string]string) string,先查缓存 channel(10ms 超时),未命中则从"模拟数据库"(500ms)查询并回填缓存。要求用 select + time.After 实现分级超时。 -
挑战题(难度⭐⭐⭐):实现一个日志聚合系统:3 个日志源(goroutine)各自生成不同级别的日志(INFO/WARN/ERROR),用 fan-in 聚合到一个 channel,然后用 select 做分级处理——ERROR 立即告警(打印),WARN 转存(5 条一刷),INFO 批量写入(10 条一刷)。要求:超时 flush + done channel 优雅退出。