go-concurrency
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Concurrency
Go并发
This skill covers concurrency patterns and best practices from Google's Go Style
Guide and Uber's Go Style Guide, including goroutine management, channel usage,
mutex handling, and synchronization.
本技能涵盖了Google Go风格指南和Uber Go风格指南中的并发模式与最佳实践,包括goroutine管理、channel使用、互斥锁处理以及同步机制。
Goroutine Lifetimes
Goroutine生命周期
Normative: When you spawn goroutines, make it clear when or whether they exit.
规范性要求:创建goroutine时,需明确其退出的时间或条件。
Why Goroutine Lifetimes Matter
为什么Goroutine生命周期很重要
Goroutines can leak by blocking on channel sends or receives. The garbage
collector will not terminate a goroutine blocked on a channel even if no
other goroutine has a reference to the channel.
Even when goroutines do not leak, leaving them in-flight when no longer needed
causes:
- Panics: Sending on a closed channel causes a panic
- Data races: Modifying still-in-use inputs after the result isn't needed
- Memory issues: Unpredictable memory usage from long-lived goroutines
- Resource leaks: Preventing unused objects from being garbage collected
go
// Bad: Sending on closed channel causes panic
ch := make(chan int)
ch <- 42
close(ch)
ch <- 13 // panicGoroutine可能会因阻塞在channel的发送或接收操作上而发生泄漏。即使没有其他goroutine持有该channel的引用,垃圾回收器也不会终止阻塞在channel上的goroutine。
即使Goroutine没有泄漏,在不再需要时仍让其运行也会导致以下问题:
- 恐慌错误:向已关闭的channel发送数据会引发panic
- 数据竞争:在结果不再需要后修改仍在使用的输入
- 内存问题:长期运行的goroutine导致不可预测的内存占用
- 资源泄漏:阻止未使用的对象被垃圾回收
go
// Bad: Sending on closed channel causes panic
ch := make(chan int)
ch <- 42
close(ch)
ch <- 13 // panicMake Lifetimes Obvious
让生命周期清晰可见
Write concurrent code so goroutine lifetimes are evident. Keep
synchronization-related code constrained within function scope and factor logic
into synchronous functions.
go
// Good: Goroutine lifetimes are clear
func (w *Worker) Run(ctx context.Context) error {
var wg sync.WaitGroup
for item := range w.q {
wg.Add(1)
go func() {
defer wg.Done()
process(ctx, item) // Returns when context is cancelled
}()
}
wg.Wait() // Prevent spawned goroutines from outliving this function
}go
// Bad: Careless about when goroutines finish
func (w *Worker) Run() {
for item := range w.q {
go process(item) // When does this finish? What if it never does?
}
}编写并发代码时,需确保Goroutine的生命周期一目了然。将同步相关代码限制在函数作用域内,并将逻辑拆分为同步函数。
go
// Good: Goroutine lifetimes are clear
func (w *Worker) Run(ctx context.Context) error {
var wg sync.WaitGroup
for item := range w.q {
wg.Add(1)
go func() {
defer wg.Done()
process(ctx, item) // Returns when context is cancelled
}()
}
wg.Wait() // Prevent spawned goroutines from outliving this function
}go
// Bad: Careless about when goroutines finish
func (w *Worker) Run() {
for item := range w.q {
go process(item) // When does this finish? What if it never does?
}
}Don't Fire-and-Forget Goroutines
不要使用即发即弃的Goroutine
Every goroutine must have a predictable stop mechanism:
- A predictable time at which it will stop running, OR
- A way to signal that it should stop
- Code must be able to block and wait for the goroutine to finish
Source: Uber Go Style Guide
go
// Bad: No way to stop or wait for this goroutine
go func() {
for {
flush()
time.Sleep(delay)
}
}()go
// Good: Stop/done channel pattern for controlled shutdown
var (
stop = make(chan struct{}) // tells the goroutine to stop
done = make(chan struct{}) // tells us that the goroutine exited
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// To shut down:
close(stop) // signal the goroutine to stop
<-done // and wait for it to exit每个Goroutine都必须有可预测的停止机制:
- 可预测的停止运行时间,或者
- 一种可以通知其停止的方式
- 代码必须能够阻塞并等待Goroutine完成
来源:Uber Go风格指南
go
// Bad: No way to stop or wait for this goroutine
go func() {
for {
flush()
time.Sleep(delay)
}
}()go
// Good: Stop/done channel pattern for controlled shutdown
var (
stop = make(chan struct{}) // tells the goroutine to stop
done = make(chan struct{}) // tells us that the goroutine exited
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// To shut down:
close(stop) // signal the goroutine to stop
<-done // and wait for it to exitWaiting for Goroutines
等待Goroutine完成
Use for multiple goroutines:
sync.WaitGroupgo
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// work...
}()
}
wg.Wait()Use a done channel for a single goroutine:
go
done := make(chan struct{})
go func() {
defer close(done)
// work...
}()
<-done // wait for goroutine to finish针对多个Goroutine,使用:
sync.WaitGroupgo
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// work...
}()
}
wg.Wait()针对单个Goroutine,使用done channel:
go
done := make(chan struct{})
go func() {
defer close(done)
// work...
}()
<-done // wait for goroutine to finishNo Goroutines in init()
不要在init()中创建Goroutine
init()CloseStopShutdownSource: Uber Go Style Guide
go
// Bad: Spawns uncontrollable background goroutine
func init() {
go doWork()
}go
// Good: Explicit lifecycle management
type Worker struct {
stop chan struct{}
done chan struct{}
}
func NewWorker() *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go w.doWork()
return w
}
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}init()CloseStopShutdown来源:Uber Go风格指南
go
// Bad: Spawns uncontrollable background goroutine
func init() {
go doWork()
}go
// Good: Explicit lifecycle management
type Worker struct {
stop chan struct{}
done chan struct{}
}
func NewWorker() *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
}
go w.doWork()
return w
}
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}Testing for Goroutine Leaks
测试Goroutine泄漏
Use go.uber.org/goleak to test for
goroutine leaks in packages that spawn goroutines.
Principle: Never start a goroutine without knowing how it will stop.
Zero-value Mutexes
零值互斥锁
The zero-value of and is valid, so you almost never
need a pointer to a mutex.
sync.Mutexsync.RWMutexSource: Uber Go Style Guide
go
// Bad: Unnecessary pointer
mu := new(sync.Mutex)
mu.Lock()go
// Good: Zero-value is valid
var mu sync.Mutex
mu.Lock()sync.Mutexsync.RWMutex来源:Uber Go风格指南
go
// Bad: Unnecessary pointer
mu := new(sync.Mutex)
mu.Lock()go
// Good: Zero-value is valid
var mu sync.Mutex
mu.Lock()Don't Embed Mutexes
不要嵌入互斥锁
If you use a struct by pointer, the mutex should be a non-pointer field. Do not
embed the mutex on the struct, even if the struct is not exported.
go
// Bad: Embedded mutex exposes Lock/Unlock as part of API
type SMap struct {
sync.Mutex // Lock() and Unlock() become methods of SMap
data map[string]string
}
func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
}go
// Good: Named field keeps mutex as implementation detail
type SMap struct {
mu sync.Mutex
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}With the bad example, and methods are unintentionally part of
the exported API. With the good example, the mutex is an implementation detail
hidden from callers.
LockUnlock如果通过指针使用结构体,互斥锁应作为非指针字段。不要将互斥锁嵌入结构体,即使结构体未导出。
go
// Bad: Embedded mutex exposes Lock/Unlock as part of API
type SMap struct {
sync.Mutex // Lock() and Unlock() become methods of SMap
data map[string]string
}
func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
}go
// Good: Named field keeps mutex as implementation detail
type SMap struct {
mu sync.Mutex
data map[string]string
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
}在不好的示例中,和方法会无意中成为导出API的一部分。在良好的示例中,互斥锁是隐藏在调用者视野之外的实现细节。
LockUnlockSynchronous Functions
同步函数
Normative: Prefer synchronous functions over asynchronous functions.
规范性要求:优先使用同步函数而非异步函数。
Why Prefer Synchronous Functions?
为什么优先选择同步函数?
- Localized goroutines: Keeps goroutines within a call, making lifetimes easier to reason about
- Avoids leaks and races: Easier to prevent resource leaks and data races
- Easier to test: Caller can pass input and check output without polling
- Caller flexibility: Caller can add concurrency by calling in a separate goroutine
go
// Good: Synchronous function - caller controls concurrency
func ProcessItems(items []Item) ([]Result, error) {
var results []Result
for _, item := range items {
result, err := processItem(item)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
// Caller can add concurrency if needed:
go func() {
results, err := ProcessItems(items)
// handle results
}()Advisory: It is quite difficult (sometimes impossible) to remove unnecessary concurrency at the caller side. Let the caller add concurrency when needed.
- Goroutine本地化:将Goroutine限制在一次调用内,使其生命周期更易于推理
- 避免泄漏和竞争:更容易防止资源泄漏和数据竞争
- 更易于测试:调用者可以传入输入并检查输出,无需轮询
- 调用者灵活性:调用者可以通过在单独的Goroutine中调用来添加并发
go
// Good: Synchronous function - caller controls concurrency
func ProcessItems(items []Item) ([]Result, error) {
var results []Result
for _, item := range items {
result, err := processItem(item)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
// Caller can add concurrency if needed:
go func() {
results, err := ProcessItems(items)
// handle results
}()建议:在调用者端移除不必要的并发是相当困难的(有时甚至不可能)。让调用者在需要时添加并发。
Channel Direction
Channel方向
Normative: Specify channel direction where possible.
规范性要求:尽可能指定Channel方向。
Why Specify Direction?
为什么要指定方向?
- Prevents errors: Compiler catches mistakes like closing a receive-only channel
- Conveys ownership: Makes clear who sends and who receives
- Self-documenting: Function signature tells the story
go
// Good: Direction specified - clear ownership
func sum(values <-chan int) int {
total := 0
for v := range values {
total += v
}
return total
}go
// Bad: No direction - allows accidental misuse
func sum(values chan int) (out int) {
for v := range values {
out += v
}
close(values) // Bug! This compiles but shouldn't happen.
}- 防止错误:编译器会捕获诸如关闭只读Channel之类的错误
- 传递所有权:明确谁负责发送,谁负责接收
- 自文档化:函数签名即可说明用途
go
// Good: Direction specified - clear ownership
func sum(values <-chan int) int {
total := 0
for v := range values {
total += v
}
return total
}go
// Bad: No direction - allows accidental misuse
func sum(values chan int) (out int) {
for v := range values {
out += v
}
close(values) // Bug! This compiles but shouldn't happen.
}Channel Size: One or None
Channel大小:1或无缓冲
Channels should usually have a size of one or be unbuffered. Any other size must
be subject to scrutiny. Consider:
- How is the size determined?
- What prevents the channel from filling up under load?
- What happens when writers block?
Source: Uber Go Style Guide
go
// Bad: Arbitrary buffer size
c := make(chan int, 64) // "Ought to be enough for anybody!"go
// Good: Deliberate sizing
c := make(chan int, 1) // Size of one
c := make(chan int) // Unbuffered, size of zeroChannel通常应设置为大小1或无缓冲。任何其他大小都必须经过仔细审查。需考虑:
- 大小是如何确定的?
- 如何防止Channel在负载下被填满?
- 当发送者阻塞时会发生什么?
来源:Uber Go风格指南
go
// Bad: Arbitrary buffer size
c := make(chan int, 64) // "Ought to be enough for anybody!"go
// Good: Deliberate sizing
c := make(chan int, 1) // Size of one
c := make(chan int) // Unbuffered, size of zeroCommon Patterns
常见模式
go
func produce(out chan<- int) { /* send-only */ }
func consume(in <-chan int) { /* receive-only */ }
func transform(in <-chan int, out chan<- int) { /* both directions */ }go
func produce(out chan<- int) { /* send-only */ }
func consume(in <-chan int) { /* receive-only */ }
func transform(in <-chan int, out chan<- int) { /* both directions */ }Atomic Operations
原子操作
Use go.uber.org/atomic for type-safe
atomic operations. The standard package operates on raw types
(, , etc.), making it easy to forget to use atomic operations
consistently.
sync/atomicint32int64Source: Uber Go Style Guide
go
// Bad: Easy to forget atomic operation
type foo struct {
running int32 // atomic
}
func (f *foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race! forgot atomic.LoadInt32
}go
// Good: Type-safe atomic operations
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running.Load() // can't accidentally read non-atomically
}The package adds type safety by hiding the underlying type
and includes convenient types like , , etc.
go.uber.org/atomicatomic.Boolatomic.Int64来源:Uber Go风格指南
go
// Bad: Easy to forget atomic operation
type foo struct {
running int32 // atomic
}
func (f *foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race! forgot atomic.LoadInt32
}go
// Good: Type-safe atomic operations
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
return // already running
}
// start the Foo
}
func (f *foo) isRunning() bool {
return f.running.Load() // can't accidentally read non-atomically
}go.uber.org/atomicatomic.Boolatomic.Int64Documenting Concurrency
并发文档编写
Advisory: Document thread-safety when it's not obvious from the operation type.
Go users assume read-only operations are safe for concurrent use, and mutating
operations are not. Document concurrency when:
- Read vs mutating is unclear - e.g., a that mutates LRU state
Lookup - API provides synchronization - e.g., thread-safe clients
- Interface has concurrency requirements - document in type definition
建议:当线程安全性从操作类型中不明显时,需进行文档说明。
Go用户默认认为只读操作可安全地并发使用,而修改操作则不能。在以下情况需编写并发相关文档:
- 读取与修改的边界不清晰 - 例如,会修改LRU状态的操作
Lookup - API提供同步机制 - 例如,线程安全的客户端
- 接口有并发要求 - 在类型定义中说明
Buffer Pooling with Channels
使用Channel实现缓冲池
Use a buffered channel as a free list to reuse allocated buffers. This "leaky
buffer" pattern uses with for non-blocking operations.
selectdefaultSee references/BUFFER-POOLING.md for the full
pattern with examples and production alternatives using .
sync.Pool使用缓冲Channel作为空闲列表来重用已分配的缓冲区。这种“泄漏缓冲”模式使用带有的来实现非阻塞操作。
defaultselect有关完整模式、示例以及使用的生产级替代方案,请参阅references/BUFFER-POOLING.md。
sync.PoolQuick Reference
快速参考
| Topic | Guidance | Type |
|---|---|---|
| Goroutine lifetimes | Make exit conditions clear | Normative |
| Fire-and-forget | Don't do it - always have stop mechanism | Normative |
| Zero-value mutexes | Valid; don't use pointers | Advisory |
| Mutex embedding | Don't embed; use named field | Advisory |
| Synchronous functions | Prefer over async | Normative |
| Channel direction | Always specify | Normative |
| Channel size | One or none by default | Advisory |
| Atomic operations | Use go.uber.org/atomic | Advisory |
| Concurrency docs | Document when not obvious | Advisory |
| 主题 | 指导原则 | 类型 |
|---|---|---|
| Goroutine生命周期 | 明确退出条件 | 规范性要求 |
| 即发即弃Goroutine | 禁止使用 - 始终要有停止机制 | 规范性要求 |
| 零值互斥锁 | 有效;不要使用指针 | 建议 |
| 互斥锁嵌入 | 不要嵌入;使用命名字段 | 建议 |
| 同步函数 | 优先于异步函数 | 规范性要求 |
| Channel方向 | 始终指定 | 规范性要求 |
| Channel大小 | 默认设为1或无缓冲 | 建议 |
| 原子操作 | 使用go.uber.org/atomic | 建议 |
| 并发文档 | 当不明显时进行说明 | 建议 |
Concurrency Checklist
并发检查清单
Before spawning a goroutine, answer:
- How will this goroutine exit?
- Can I signal it to stop?
- Can I wait for it to finish?
- Who owns the channels it uses?
- What happens when the context is cancelled?
- Should this be a synchronous function instead?
在创建Goroutine之前,请回答:
- 这个Goroutine将如何退出?
- 我能否通知它停止?
- 我能否等待它完成?
- 它使用的Channel归谁所有?
- 当上下文被取消时会发生什么?
- 这个操作是否应该用同步函数实现?
Anti-Patterns to Avoid
需避免的反模式
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Fire-and-forget goroutines | Resource leaks, undefined behavior | Use WaitGroup, done channel, or context |
| Goroutines in init() | Uncontrollable lifecycle | Use explicit object with Shutdown method |
| Embedded mutexes | Leaks Lock/Unlock into API | Use named |
| Pointer to mutex | Unnecessary indirection | Zero-value is valid |
| Arbitrary channel buffers | Hidden blocking issues | Default to 0 or 1 |
| Raw sync/atomic | Easy to forget atomic reads | Use go.uber.org/atomic |
| Undocumented thread-safety | Callers may race | Document when unclear |
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 即发即弃的Goroutine | 资源泄漏、未定义行为 | 使用WaitGroup、done channel或context |
| init()中的Goroutine | 生命周期不可控 | 使用带有Shutdown方法的显式对象 |
| 嵌入的互斥锁 | 将Lock/Unlock暴露到API中 | 使用命名的 |
| 互斥锁指针 | 不必要的间接引用 | 零值是有效的 |
| 任意大小的Channel缓冲区 | 隐藏的阻塞问题 | 默认设为0或1 |
| 原生sync/atomic | 容易忘记原子读取 | 使用go.uber.org/atomic |
| 未文档化的线程安全性 | 调用者可能引发竞争 | 在不清晰时进行文档说明 |
See Also
另请参阅
- go-style-core: Foundational style principles (clarity, simplicity)
- go-error-handling: Error handling patterns in concurrent code
- go-defensive: Defensive programming including validation and safety
- go-documentation: General documentation conventions
- go-style-core:基础风格原则(清晰性、简洁性)
- go-error-handling:并发代码中的错误处理模式
- go-defensive:防御式编程,包括验证与安全
- go-documentation:通用文档规范
External Resources
外部资源
- Never start a goroutine without knowing how it will
stop
- Dave Cheney
- Rethinking Classical Concurrency Patterns - Bryan Mills (GopherCon 2018)
- When Go programs end - Go Time podcast
- go.uber.org/goleak - Goroutine leak detector for testing
- go.uber.org/atomic - Type-safe atomic operations
- Never start a goroutine without knowing how it will stop
- Dave Cheney
- Rethinking Classical Concurrency Patterns - Bryan Mills (GopherCon 2018)
- When Go programs end - Go Time播客
- go.uber.org/goleak - 用于测试的Goroutine泄漏检测器
- go.uber.org/atomic - 类型安全的原子操作库