外观
Sync and Context
Phase 02 — Go Core 涵盖:
WaitGroup·Mutex·Context
1. 学习目标
完成本知识点后,你应该能够:
- 使用
sync.WaitGroup等待多个 goroutine 完成 - 使用
sync.Mutex/sync.RWMutex保护共享数据 - 理解
context.Context的取消、超时和传值 - 在 HTTP handler 和并发任务中正确传递 context
- 避免数据竞争和 goroutine 泄漏
- 在数字孪生后端中实现可取消的批量数据采集
2. 为什么需要
channel 适合通信,但不总是最简方案。多个 goroutine 只需「等全部做完」时,WaitGroup 比 channel 更直接。多个 goroutine 读写同一份 map 或 counter 时,需要 Mutex 防止 data race。
HTTP 请求有超时:客户端断开或超过 30 秒,后端应停止无意义的 DB 查询和设备轮询。Context 是 Go 标准的取消信号传递机制,net/http 每个 Request 都带 r.Context()。
3. 核心概念
3.1 sync.WaitGroup
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
wg.Wait() // 阻塞直到 counter 归零3.2 sync.Mutex
go
var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()RWMutex:多读单写,读多写少时更高效。
3.3 context.Context
| 方法 | 用途 |
|---|---|
context.Background() | 根 context |
context.WithCancel(parent) | 手动取消 |
context.WithTimeout(parent, d) | 超时自动取消 |
context.WithValue(parent, key, val) | 传值(谨慎使用) |
ctx.Done() | 接收取消信号的 channel |
ctx.Err() | 取消原因 |
3.4 使用原则
- Context 作为函数第一个参数:
func Do(ctx context.Context, ...) - 不要存到 struct 里长期持有
- WithValue 只传 request-scoped 元数据(trace ID),不传业务参数
4. 基础语法
完整示例见 workspace/phase-02/sync-context/main.go。
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
type DeviceStore struct {
mu sync.RWMutex
devices map[string]float64
}
func (s *DeviceStore) Set(id string, temp float64) {
s.mu.Lock()
defer s.mu.Unlock()
s.devices[id] = temp
}
func (s *DeviceStore) Get(id string) (float64, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
v, ok := s.devices[id]
return v, ok
}
func fetchWithContext(ctx context.Context, id string, wg *sync.WaitGroup, store *DeviceStore) {
defer wg.Done()
select {
case <-time.After(200 * time.Millisecond):
store.Set(id, 36.5)
fmt.Println("fetched", id)
case <-ctx.Done():
fmt.Println("cancelled", id, ctx.Err())
}
}
func main() {
store := &DeviceStore{devices: make(map[string]float64)}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
ids := []string{"s1", "s2", "s3"}
var wg sync.WaitGroup
for _, id := range ids {
wg.Add(1)
go fetchWithContext(ctx, id, &wg, store)
}
wg.Wait()
fmt.Println("done, err:", ctx.Err())
}5. 代码解析
go
defer wg.Done()WaitGroup 的 Add/Done 必须配对。在 goroutine 入口 defer Done 是惯用法,即使 panic 也会执行(配合 recover 使用)。
go
s.mu.Lock()
defer s.mu.Unlock()Mutex 锁定范围尽量小。defer Unlock 防止忘记解锁。
go
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()必须 defer cancel(),释放 timer 资源,即使超时已触发。
go
select {
case <-ctx.Done():
return ctx.Err()
default:
// continue work
}长循环中定期检查 ctx.Done(),及时响应取消。
6. JavaScript / TypeScript 对比
| 概念 | Go | JavaScript |
|---|---|---|
| 等待多个任务 | WaitGroup | Promise.all |
| 互斥锁 | sync.Mutex | 无(单线程,Worker 需 Atomics) |
| 取消信号 | context.Context | AbortController |
| 超时 | context.WithTimeout | AbortSignal.timeout / setTimeout |
| 请求作用域 | r.Context() | req.signal(Fetch API) |
关键差异:
- Go 多 goroutine 真正并行,Mutex 是必需品;JS 主线程无此问题
- Context 是显式传递的,不像 AbortSignal 可挂在 fetch 上自动传播
go test -race可检测 data race,开发阶段应开启
7. 常见错误
错误 1:WaitGroup 重复 Add 或负 counter
go
wg.Add(1)
wg.Add(1) // 要在 go 之前 Add
go work()错误 2:复制 Mutex
go
type Bad struct{ mu sync.Mutex }
b2 := b1 // ❌ Mutex 不可复制
// 用指针 *Bad 或嵌入 sync.Mutex错误 3:忘记 defer cancel()
go
ctx, cancel := context.WithTimeout(...)
// 忘记 cancel() → 资源泄漏直到超时错误 4:用 Context 传大量业务数据
go
ctx = context.WithValue(ctx, "user", hugeUserObject) // ❌ 反模式
// 业务参数应显式传参错误 5:锁粒度过大
整个 HTTP handler 加锁会降低并发度;只锁必要的临界区。
8. 实际应用
数字孪生 — 可取消的批量采集
go
func CollectAll(ctx context.Context, ids []string) ([]Reading, error) {
var wg sync.WaitGroup
results := make([]Reading, 0, len(ids))
var mu sync.Mutex
for _, id := range ids {
if ctx.Err() != nil {
break
}
wg.Add(1)
go func(id string) {
defer wg.Done()
reading, err := pollDevice(ctx, id)
if err != nil {
return
}
mu.Lock()
results = append(results, reading)
mu.Unlock()
}(id)
}
wg.Wait()
return results, ctx.Err()
}客户端断开 → HTTP context 取消 → 所有 poll goroutine 退出。
HTTP 与 Context
go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data, err := service.Query(ctx, id)
if errors.Is(err, context.Canceled) {
return // 客户端已断开
}
}9. 深入理解
9.1 data race
两个 goroutine 并发访问同一变量,至少一个是写,且无同步 → data race。用 -race 检测。
9.2 sync.Once
单次初始化:sync.Once 保证 Do 内的函数只执行一次,常用于 singleton。
9.3 Context 树
子 context 取消会传播到所有子孙。WithTimeout 到期自动 cancel 子树。
9.4 Mutex vs channel
Go 谚语:「用 channel 通信,用 Mutex 保护状态」。计数器、map 用 Mutex 更简单;任务分发用 channel。
10. 练习
详细题目见
exercises/phase-02-core/06-sync-context.md。
Level 1 — 基础
练习 1.1:3 个 goroutine 各 sleep 不同时间,WaitGroup 等待全部完成。
练习 1.2:10 个 goroutine 各 +1 counter,用 Mutex 保护,最终 counter=10。
练习 1.3:WithTimeout 1 秒,2 秒任务应被取消。
Level 2 — 应用
练习 2.1:RWMutex 保护 map:多读单写。
练习 2.2:长循环监听 ctx.Done() 优雅退出。
练习 2.3:WithCancel 手动取消所有 worker。
Level 3 — 综合
练习 3.1:并发写 DeviceStore,读回全部数据无 race。
练习 3.2:模拟 HTTP:父 context 取消,子 goroutine 全部停止。
Level 4 — 项目实践
练习 4.1:在 workspace/phase-02/sync-context/ 构建可取消的 telemetry 采集服务:Mutex 存储 + WaitGroup 等待 + Context 超时。
11. 学习检查
- WaitGroup 的 Add 应该在 go 之前还是之后调用?
- 为什么 Mutex 不能复制?
context.WithTimeout为什么必须 defer cancel()?- Context 应该存在 struct 里吗?
- 什么情况下用 Mutex,什么情况下用 channel?
- 如何检测程序中的 data race?
12. 下一步
| 已完成 | 下一知识点 | 关系 |
|---|---|---|
| WaitGroup, Mutex, Context | Phase 03 HTTP | 每个请求自带 Context |
| Phase 06 WebSocket | 长连接更依赖 Context 取消 |
Phase 02 完成!进入 Phase 03,用 net/http 构建 REST API,对接 Vue 前端。