Go 并发三板斧:如何优雅地收集多个 goroutine 的结果?
#
## 写在前面
在 Go 里写并发程序,最常见的一个需求是:**同时发起多个任务,然后收集它们的结果。**
但收集的方式不止一种。你是**全部等齐再处理**?还是**谁先回来用谁的**?还是**边回来边处理**?
这三种场景,对应了 Go 并发编程里三种不同的模式。这篇文章带你一次性搞清楚。
---
## 场景设定
假设我们有一个酒店后台系统,需要同时查询三个数据源:
- 房价(`fetchPrice`)
- 房态(`fetchRoomStatus`)
- 客户偏好(`fetchPreference`)每个查询耗时 1-3 秒不等,我们希望能用并发的方式加速。
```go
type QueryResult struct {
Price string
Room string
Prefer string
Err error
}
```---
## 模式一:全部等齐再处理(`sync.WaitGroup`)
这是最经典的用法:**等所有任务都完成,再统一返回。**
```go
func queryAll(ctx context.Context, room string) QueryResult {
var wg sync.WaitGroup
var mu sync.Mutex
result := QueryResult{}wg.Add(3)
go func() {
defer wg.Done()
price, err := fetchPrice(ctx, room)
mu.Lock()
result.Price = price
result.Err = err
mu.Unlock()
}()go func() {
defer wg.Done()
roomStatus, err := fetchRoomStatus(ctx, room)
mu.Lock()
result.Room = roomStatus
if err != nil {
result.Err = err
}
mu.Unlock()
}()go func() {
defer wg.Done()
prefer, err := fetchPreference(ctx, room)
mu.Lock()
result.Prefer = prefer
if err != nil {
result.Err = err
}
mu.Unlock()
}()wg.Wait()
return result
}
```**特点**:
- 三个任务全部完成后,`wg.Wait()` 才放行
- 总耗时 = 最慢的那个任务
- 适合需要完整数据的场景(如生成报表、页面渲染)---
## 模式二:谁先完成用谁的(一次 `select`)
如果你只关心最快的那个结果,可以用 `select` 取第一个回来的数据。
```go
func queryAny(ctx context.Context, room string) QueryResult {
ch := make(chan QueryResult, 3)go func() {
price, err := fetchPrice(ctx, room)
ch <- QueryResult{Price: price, Err: err}
}()
go func() {
roomStatus, err := fetchRoomStatus(ctx, room)
ch <- QueryResult{Room: roomStatus, Err: err}
}()
go func() {
prefer, err := fetchPreference(ctx, room)
ch <- QueryResult{Prefer: prefer, Err: err}
}()select {
case result := <-ch:
return result
case <-ctx.Done():
return QueryResult{Err: ctx.Err()}
}
}
```**特点**:
- 只取第一个返回的结果,其余忽略
- 总耗时 = 最快的那个任务
- 适合竞争式查询(如查多个缓存、多个供应商报价)---
## 模式三:边完成边收集(循环 `select`)
如果你既要**全部结果**,又想**边完成边处理**,可以用 `for` + `select` 循环。
```go
func queryAllWithSelect(ctx context.Context, room string) QueryResult {
ch := make(chan QueryResult, 3)go func() {
price, err := fetchPrice(ctx, room)
ch <- QueryResult{Price: price, Err: err}
}()
go func() {
roomStatus, err := fetchRoomStatus(ctx, room)
ch <- QueryResult{Room: roomStatus, Err: err}
}()
go func() {
prefer, err := fetchPreference(ctx, room)
ch <- QueryResult{Prefer: prefer, Err: err}
}()result := QueryResult{}
for i := 0; i < 3; i++ {
select {
case part := <-ch:
if part.Price != "" {
result.Price = part.Price
}
if part.Room != "" {
result.Room = part.Room
}
if part.Prefer != "" {
result.Prefer = part.Prefer
}
if part.Err != nil && result.Err == nil {
result.Err = part.Err
}
case <-ctx.Done():
result.Err = ctx.Err()
return result
}
}
return result
}
```**特点**:
- 所有结果都能收到
- 可以边收边处理(如实时更新 UI 进度条)
- 总耗时 = 最慢的那个任务,但不会卡死(有超时保护)---
## 三种模式对比一览
| 模式 | 工具 | 结果完整性 | 总耗时 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- |
| 全部等齐 | `sync.WaitGroup` | ✅ 全部 | 最慢的任务 | 报表、完整页面 |
| 取最快 | 一次 `select` | ❌ 一个 | 最快的任务 | 缓存竞争、降级 |
| 边收边等 | 循环 `select` | ✅ 全部 | 最慢的任务 | 进度展示、实时更新 |---
## 总结
Go 的并发模型并不复杂,但“收集结果”这个看似简单的需求,其实有几种不同的实现方式。
**选择哪个,取决于你的业务需求:**
- 需要全部数据?👉 用 `WaitGroup`
- 只要最快的那一个?👉 用 `select`
- 全部都要,还要实时反馈?👉 用循环 `select`这三种模式,你学会了吗?