go-concurrency-review
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Concurrency Review
Go 并发安全模式评审
Concurrency in Go is powerful and deceptively easy to get wrong.
These patterns prevent goroutine leaks, data races, and deadlocks.
Go语言的并发功能强大,但也极易出错。以下这些模式可避免goroutine泄漏、数据竞态和死锁问题。
1. Goroutine Lifecycle Management
1. Goroutine 生命周期管理
EVERY goroutine MUST have a clear termination path. No fire-and-forget.
每个goroutine都必须有明确的终止路径,禁止"启动后不管"的写法。
Use errgroup
for coordinated goroutines:
errgroup使用errgroup
实现协程协同:
errgroupgo
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch data: %w", err)
}go
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchUsers(ctx)
})
g.Go(func() error {
return fetchOrders(ctx)
})
if err := g.Wait(); err != nil {
return fmt.Errorf("fetch data: %w", err)
}Long-running goroutines must respect context:
长期运行的goroutine必须响应上下文:
go
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
if err := w.process(job); err != nil {
w.logger.Error("process job", zap.Error(err))
}
}
}
}go
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
if err := w.process(job); err != nil {
w.logger.Error("process job", zap.Error(err))
}
}
}
}Start goroutines in the owner, not the callee:
在调用方启动goroutine,而非被调用方:
go
// ✅ Good — caller controls lifecycle
go worker.Run(ctx)
// ❌ Bad — function secretly starts goroutine
func NewWorker() *Worker {
w := &Worker{}
go w.run() // hidden goroutine — caller has no control
return w
}go
// ✅ 推荐做法 — 调用方控制生命周期
go worker.Run(ctx)
// ❌ 错误做法 — 函数内部偷偷启动goroutine
func NewWorker() *Worker {
w := &Worker{}
go w.run() // 隐藏的goroutine — 调用方无法控制
return w
}2. Channel Patterns
2. 通道(Channel)模式
Channel size is one or none:
通道大小设为1或不设置缓冲:
go
// Unbuffered — synchronization point
ch := make(chan Result)
// Buffered with size 1 — single-item handoff
ch := make(chan Result, 1)
// Larger buffers need explicit justification with documented reasoning
ch := make(chan Result, 100) // requires comment explaining whygo
// 无缓冲通道 — 同步点
ch := make(chan Result)
// 缓冲大小为1的通道 — 单元素传递
ch := make(chan Result, 1)
// 更大的缓冲需要明确的注释说明理由
ch := make(chan Result, 100) // 需要注释解释原因Signal channels use empty struct:
信号通道使用空结构体:
go
done := make(chan struct{})
close(done) // broadcast signal to all receiversgo
done := make(chan struct{})
close(done) // 向所有接收者广播信号Producer/consumer with clean shutdown:
支持优雅关闭的生产者/消费者模式:
go
func produce(ctx context.Context) <-chan Item {
ch := make(chan Item)
go func() {
defer close(ch)
for {
item, err := fetchNext(ctx)
if err != nil {
return
}
select {
case ch <- item:
case <-ctx.Done():
return
}
}
}()
return ch
}go
func produce(ctx context.Context) <-chan Item {
ch := make(chan Item)
go func() {
defer close(ch)
for {
item, err := fetchNext(ctx)
if err != nil {
return
}
select {
case ch <- item:
case <-ctx.Done():
return
}
}
}()
return ch
}3. Mutex Patterns
3. 互斥锁(Mutex)模式
Zero-value mutexes are valid:
零值互斥锁是合法的:
go
// ✅ Good — zero value works
type Cache struct {
mu sync.RWMutex
items map[string]Item
}
// ❌ Bad — unnecessary pointer
type Cache struct {
mu *sync.RWMutex // never do this
}go
// ✅ 推荐做法 — 零值即可使用
type Cache struct {
mu sync.RWMutex
items map[string]Item
}
// ❌ 错误做法 — 不必要的指针
type Cache struct {
mu *sync.RWMutex // 绝对不要这么做
}Mutex placement in struct:
互斥锁在结构体中的位置:
go
type SafeMap struct {
mu sync.RWMutex // mutex guards the fields below
items map[string]string
count int
}The mutex should appear directly above the field(s) it protects,
with a comment indicating the relationship.
go
type SafeMap struct {
mu sync.RWMutex // 该互斥锁保护下方的字段
items map[string]string
count int
}互斥锁应该直接放在它所保护的字段上方,并添加注释说明两者的关系。
Lock scope should be minimal:
锁的作用域应尽可能小:
go
// ✅ Good — minimal lock scope
func (c *Cache) Get(key string) (Item, bool) {
c.mu.RLock()
item, ok := c.items[key]
c.mu.RUnlock()
return item, ok
}
// ✅ Also good — defer for methods that return early
func (c *Cache) GetOrCreate(key string) Item {
c.mu.Lock()
defer c.mu.Unlock()
if item, ok := c.items[key]; ok {
return item
}
item := newItem(key)
c.items[key] = item
return item
}go
// ✅ 推荐做法 — 最小化锁作用域
func (c *Cache) Get(key string) (Item, bool) {
c.mu.RLock()
item, ok := c.items[key]
c.mu.RUnlock()
return item, ok
}
// ✅ 同样推荐 — 提前返回的方法使用defer解锁
func (c *Cache) GetOrCreate(key string) Item {
c.mu.Lock()
defer c.mu.Unlock()
if item, ok := c.items[key]; ok {
return item
}
item := newItem(key)
c.items[key] = item
return item
}Never copy mutexes:
绝对不要复制互斥锁:
go
// ❌ BLOCKER — copying a mutex copies its lock state
cache2 := *cache1 // this copies the mutex!go
// ❌ 严重问题 — 复制互斥锁会复制其锁状态
cache2 := *cache1 // 这会复制互斥锁!4. Atomic Operations
4. 原子操作
Use or for simple counters and flags:
sync/atomicgo.uber.org/atomicgo
// ✅ Good — type-safe atomics
import "go.uber.org/atomic"
type Server struct {
running atomic.Bool
reqCount atomic.Int64
}
func (s *Server) HandleRequest() {
s.reqCount.Inc()
// ...
}使用或实现简单计数器和标志位:
sync/atomicgo.uber.org/atomicgo
// ✅ 推荐做法 — 类型安全的原子操作
import "go.uber.org/atomic"
type Server struct {
running atomic.Bool
reqCount atomic.Int64
}
func (s *Server) HandleRequest() {
s.reqCount.Inc()
// ...
}5. Context Propagation
5. 上下文(Context)传播
Rules:
规则:
- Context is ALWAYS the first parameter.
- Never store context in a struct field.
- Derive child contexts for sub-operations:
go
func (s *Service) Process(ctx context.Context, req Request) error {
// Derive context with timeout for external call
fetchCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // ALWAYS defer cancel
data, err := s.client.Fetch(fetchCtx, req.ID)
if err != nil {
return fmt.Errorf("fetch %s: %w", req.ID, err)
}
// ...
}- 上下文永远作为第一个参数。
- 绝对不要将上下文存储在结构体字段中。
- 为子操作派生上下文:
go
func (s *Service) Process(ctx context.Context, req Request) error {
// 为外部调用派生带超时的上下文
fetchCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // 必须使用defer执行cancel
data, err := s.client.Fetch(fetchCtx, req.ID)
if err != nil {
return fmt.Errorf("fetch %s: %w", req.ID, err)
}
// ...
}NEVER ignore context cancellation in select:
绝对不要在select中忽略上下文取消信号:
go
// ✅ Good
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ Bad — blocks forever if context cancelled
result := <-chgo
// ✅ 推荐做法
select {
case result := <-ch:
return result, nil
case <-ctx.Done():
return nil, ctx.Err()
}
// ❌ 错误做法 — 上下文取消后会永久阻塞
result := <-ch6. Avoid Mutable Globals
6. 避免使用可变全局变量
go
// ❌ Bad — mutable global, not safe for concurrent access
var db *sql.DB
// ✅ Good — pass as dependency
type Server struct {
db *sql.DB
}go
// ❌ 错误做法 — 可变全局变量,并发访问不安全
var db *sql.DB
// ✅ 推荐做法 — 作为依赖传入
type Server struct {
db *sql.DB
}7. sync.Once for Lazy Initialization
7. 使用sync.Once实现延迟初始化
go
type Client struct {
initOnce sync.Once
conn *grpc.ClientConn
}
func (c *Client) getConn() *grpc.ClientConn {
c.initOnce.Do(func() {
c.conn = dial()
})
return c.conn
}go
type Client struct {
initOnce sync.Once
conn *grpc.ClientConn
}
func (c *Client) getConn() *grpc.ClientConn {
c.initOnce.Do(func() {
c.conn = dial()
})
return c.conn
}Race Detection
竞态条件检测
ALWAYS run tests with race detector during CI:
bash
go test -race ./...This is non-negotiable. A test suite that passes without proves nothing
about concurrent correctness.
-race必须在CI流程中使用竞态检测器运行测试:
bash
go test -race ./...这是硬性要求。没有使用参数通过的测试套件,无法证明其并发正确性。
-raceRed Flags Checklist
风险信号检查清单
- 🔴 Goroutine started without shutdown path
- 🔴 Channel never closed (potential goroutine leak)
- 🔴 Mutex copied by value
- 🔴 Context stored in struct field
- 🔴 used where parent context was available
context.Background() - 🔴 without
selectcase in blocking operationctx.Done() - 🔴 Shared map/slice accessed without synchronization
- 🟡 Buffered channel with arbitrary large size
- 🟡 used for synchronization instead of proper signaling
time.Sleep - 🟡 Goroutine starting inside or constructor without lifecycle control
init()
- 🔴 启动goroutine但未提供关闭路径
- 🔴 通道从未关闭(可能导致goroutine泄漏)
- 🔴 按值复制互斥锁
- 🔴 将上下文存储在结构体字段中
- 🔴 已有父上下文的情况下使用
context.Background() - 🔴 阻塞操作的select语句中没有分支
ctx.Done() - 🔴 未加同步地访问共享map/slice
- 🟡 使用任意大的缓冲通道
- 🟡 使用进行同步而非正确的信号机制
time.Sleep - 🟡 在函数或构造函数中启动goroutine且未提供生命周期控制
init()