mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
192 lines
7.7 KiB
Markdown
192 lines
7.7 KiB
Markdown
---
|
||
name: go-concurrency
|
||
description: Use when writing concurrent Go code — goroutines, channels, mutexes, or thread-safety guarantees. Also use when parallelizing work, fixing data races, or protecting shared state, even if the user doesn't explicitly mention concurrency primitives. Does not cover context.Context patterns (see go-context).
|
||
license: Apache-2.0
|
||
compatibility: Requires go.uber.org/atomic for atomic operation wrappers
|
||
metadata:
|
||
sources: "Effective Go, Google Style Guide, Uber Style Guide"
|
||
---
|
||
|
||
# Go 并发
|
||
|
||
## Goroutine 生命周期
|
||
|
||
> **规范**:当你启动 goroutine 时,要明确它们何时或是否退出。
|
||
|
||
Goroutine 可能因阻塞在 channel 的发送/接收上而泄漏。GC **不会终止**被阻塞的 goroutine,即使没有其他 goroutine 持有对该 channel 的引用。即使不泄漏的在途 goroutine 也会导致 panic(在已关闭的 channel 上发送)、数据竞争、内存问题和资源泄漏。
|
||
|
||
### 核心规则
|
||
|
||
1. **每个 goroutine 都需要停止机制** —— 可预测的结束时间、取消信号,或两者兼有
|
||
2. **代码必须能够等待** goroutine 完成
|
||
3. **不在 `init()` 中启动 goroutine** —— 改为暴露生命周期方法(`Close`、`Stop`、`Shutdown`)
|
||
4. **保持同步作用域化** —— 限制在函数作用域内,将逻辑分解为同步函数
|
||
|
||
```go
|
||
// 好:使用 WaitGroup 明确生命周期
|
||
var wg sync.WaitGroup
|
||
for item := range queue {
|
||
wg.Add(1)
|
||
go func() { defer wg.Done(); process(ctx, item) }()
|
||
}
|
||
wg.Wait()
|
||
```
|
||
|
||
```go
|
||
// 不好:无法停止或等待
|
||
go func() { for { flush(); time.Sleep(delay) } }()
|
||
```
|
||
|
||
使用 [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) **检测泄漏**。
|
||
|
||
> **原则**:永远不要在不知道 goroutine 将如何停止的情况下启动它。
|
||
|
||
> 在实现 stop/done channel 模式、goroutine 等待策略或
|
||
> 生命周期管理的 worker 时,阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
|
||
|
||
---
|
||
|
||
## 通过通信共享
|
||
|
||
> "不要通过共享内存来通信;而是通过通信来共享内存。"
|
||
|
||
这是 Go 并发设计的基础原则。使用 **channel** 进行所有权转移和协调 —— 当一个 goroutine 生产值,另一个消费它时使用。当多个 goroutine 访问共享状态且 channel 会增加不必要的复杂性时,使用 **互斥锁**。
|
||
|
||
**默认使用 channel。** 当问题本质上是保护共享数据结构(例如缓存或计数器)而非在 goroutine 之间传递数据时,退回到 `sync.Mutex` / `sync.RWMutex`。
|
||
|
||
---
|
||
|
||
## 同步函数
|
||
|
||
> **规范**:优先使用同步函数而非异步函数。
|
||
|
||
| 优势 | 原因 |
|
||
|---|---|
|
||
| 局部化 goroutine | 生命周期更容易推理 |
|
||
| 避免泄漏和竞争 | 更容易防止资源泄漏和数据竞争 |
|
||
| 更容易测试 | 直接检查输入/输出,无需轮询 |
|
||
| 调用方灵活性 | 调用方在需要时添加并发 |
|
||
|
||
> **建议**:在调用方移除不必要的并发是相当困难的(有时是不可能的)。让调用方在需要时添加并发。
|
||
|
||
> 在编写同步优先的 API(调用方可以将其包装在 goroutine 中)时,
|
||
> 阅读 [references/GOROUTINE-PATTERNS.md](references/GOROUTINE-PATTERNS.md)。
|
||
|
||
---
|
||
|
||
## 零值互斥锁
|
||
|
||
`sync.Mutex` 和 `sync.RWMutex` 的零值是有效的 —— 几乎不需要互斥锁的指针。
|
||
|
||
```go
|
||
// 好:零值有效 // 不好:不必要的指针
|
||
var mu sync.Mutex mu := new(sync.Mutex)
|
||
```
|
||
|
||
**不要嵌入互斥锁** —— 使用命名的 `mu` 字段,使 `Lock`/`Unlock` 保持为实现细节,而非导出的 API。
|
||
|
||
> 在实现互斥锁保护的 struct 或决定如何组织互斥锁字段时,
|
||
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||
|
||
---
|
||
|
||
## Channel 方向
|
||
|
||
> **规范**:尽可能指定 channel 方向。
|
||
|
||
方向可以防止错误(编译器会捕获对仅接收 channel 的关闭操作),传达所有权,并且具有自文档化效果。
|
||
|
||
```go
|
||
func produce(out chan<- int) { /* 仅发送 */ }
|
||
func consume(in <-chan int) { /* 仅接收 */ }
|
||
func transform(in <-chan int, out chan<- int) { /* 双向 */ }
|
||
```
|
||
|
||
### Channel 大小:一或零
|
||
|
||
Channel 的大小应为 **零**(无缓冲)或 **一**。其他任何大小都需要给出理由:
|
||
|
||
- 大小是如何确定的
|
||
- 什么机制防止 channel 在负载下填满
|
||
- 当写入者阻塞时会发生什么
|
||
|
||
```go
|
||
c := make(chan int) // 无缓冲 —— 好
|
||
c := make(chan int, 1) // 大小为 1 —— 好
|
||
c := make(chan int, 64) // 任意大小 —— 需要给出理由
|
||
```
|
||
|
||
> 在审查详细的 channel 方向示例及易出错模式时,
|
||
> 阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||
|
||
---
|
||
|
||
## 原子操作
|
||
|
||
使用 `atomic.Bool`、`atomic.Int64` 等(Go 1.19 起标准库 `sync/atomic` 提供,或 [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic))进行类型安全的原子操作。原始的 `int32`/`int64` 字段容易在某些代码路径上忘记原子访问。
|
||
|
||
```go
|
||
// 好:类型安全 // 不好:容易忘记
|
||
var running atomic.Bool var running int32 // 原子操作
|
||
running.Store(true) atomic.StoreInt32(&running, 1)
|
||
running.Load() running == 1 // 竞争!
|
||
```
|
||
|
||
> 在 sync/atomic 和 go.uber.org/atomic 之间选择,或在 struct 中实现原子
|
||
> 状态标志时,阅读 [references/SYNC-PRIMITIVES.md](references/SYNC-PRIMITIVES.md)。
|
||
|
||
---
|
||
|
||
## 并发文档
|
||
|
||
> **建议**:当线程安全性从操作类型不明显时,添加文档说明。
|
||
|
||
Go 用户假设只读操作可以安全地并发使用,而修改操作则不行。在以下情况添加并发文档:
|
||
|
||
1. **读取与修改不明确** —— 例如,会修改 LRU 状态的 `Lookup`
|
||
2. **API 提供同步** —— 例如,线程安全的客户端
|
||
3. **接口有并发要求** —— 在类型定义中添加文档
|
||
|
||
---
|
||
|
||
## Context 使用
|
||
|
||
> 有关 context.Context 的指导(参数位置、struct 存储、自定义
|
||
> 类型、派生模式),请参阅专门的
|
||
> [go-context](../go-context/SKILL.md) skill。
|
||
|
||
---
|
||
|
||
## 使用 Channel 的缓冲池
|
||
|
||
使用有缓冲 channel 作为空闲列表来复用已分配的缓冲区。这种"泄漏缓冲"模式使用带 `default` 的 `select` 进行非阻塞操作。
|
||
|
||
> 在实现带可复用缓冲区的 worker pool 或在基于 channel 的池和
|
||
> `sync.Pool` 之间选择时,阅读 [references/BUFFER-POOLING.md](references/BUFFER-POOLING.md)。
|
||
|
||
---
|
||
|
||
## 高级模式
|
||
|
||
> 在实现使用 channel 的 channel 进行请求-响应多路复用,或
|
||
> 跨核心的 CPU 密集型并行计算时,阅读 [references/ADVANCED-PATTERNS.md](references/ADVANCED-PATTERNS.md)。
|
||
|
||
---
|
||
|
||
## 相关 Skill
|
||
|
||
- **Context 传播**:在通过 goroutine 传递取消、截止时间或请求作用域值时,请参阅 [go-context](../go-context/SKILL.md)
|
||
- **错误处理**:在从 goroutine 传播错误或使用 errgroup 时,请参阅 [go-error-handling](../go-error-handling/SKILL.md)
|
||
- **防御性加固**:在 API 边界保护共享状态或使用 defer 清理时,请参阅 [go-defensive](../go-defensive/SKILL.md)
|
||
- **接口设计**:在为包含 sync 原语的类型选择接收器类型时,请参阅 [go-interfaces](../go-interfaces/SKILL.md)
|
||
|
||
### 外部资源
|
||
|
||
- [永远不要在不知道 goroutine 将如何停止的情况下启动它](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop)
|
||
—— Dave Cheney
|
||
- [重新思考经典并发模式](https://www.youtube.com/watch?v=5zXAHh5tJqQ) —— Bryan Mills
|
||
(GopherCon 2018)
|
||
- [Go 程序何时结束](https://changelog.com/gotime/165) —— Go Time 播客
|
||
- [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) —— 用于测试的 Goroutine 泄漏检测器
|
||
- [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) —— 类型安全的原子操作
|