

























虽然 Go 推崇用 channel 通信,但在某些场景下,传统的共享内存加锁同步更简洁高效。本章讲解 sync 包提供的同步原语、sync/atomic 原子操作、context 包以及并发安全的注意事项。
Go 社区有一条经验法则:
sync 包的锁。两者并非对立,应根据场景选择更清晰的方案。"通过通信共享内存"是首选,但不是教条。
sync.Mutex 是最常用的同步原语,用于保护临界区,确保同一时刻只有一个 goroutine 访问共享资源:
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // 保证解锁,即使 panic
c.count++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
Lock() 和 Unlock() 必须成对出现,通常用 defer 确保解锁。当读操作远多于写操作时,sync.RWMutex 允许多个读者同时持有读锁,但写锁是排他的,能显著提升并发读性能:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // 读锁,可并发
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // 写锁,排他
defer c.mu.Unlock()
c.data[key] = value
}
sync.WaitGroup 用于等待一组 goroutine 全部完成,是替代"用 channel 计数"的简洁方案:
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器加 1
go func(id int) {
defer wg.Done() // 完成时计数器减 1
fmt.Printf("worker %d 完成\n", id)
}(i)
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("所有 worker 已完成")
}
Add 应在启动 goroutine 之前调用,避免竞态。Done 通常用 defer 调用,确保即使 panic 也能递减。sync.Once 确保某个操作在程序运行期间仅执行一次,常用于单例初始化、懒加载:
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
instance = &Database{ /* 初始化连接 */ }
})
return instance
}
无论 GetDatabase 被多少个 goroutine 并发调用,初始化逻辑只会执行一次,且其他 goroutine 会等待初始化完成。
内置 map 不是并发安全的。对于读多写少、键集合相对稳定的场景,可使用 sync.Map:
var m sync.Map
m.Store("key", 100) // 写入
value, ok := m.Load("key") // 读取
m.Delete("key") // 删除
m.LoadOrStore("k", 1) // 不存在则存储
// 遍历
m.Range(func(key, value any) bool {
fmt.Println(key, value)
return true // 返回 false 停止遍历
})
注意:
sync.Map并非万能。对于大多数场景,"普通 map + RWMutex"性能更好、更直观。仅在特定读多写少或键稳定的场景才考虑sync.Map。
对于简单的数值操作,原子操作比互斥锁更高效,因为它直接利用 CPU 的原子指令,无需加锁:
import "sync/atomic"
var counter int64
func increment() {
atomic.AddInt64(&counter, 1) // 原子加
}
func read() int64 {
return atomic.LoadInt64(&counter) // 原子读
}
Go 1.19 起提供了更易用的原子类型封装:
var counter atomic.Int64
counter.Add(1)
counter.Load()
counter.Store(10)
counter.CompareAndSwap(10, 20) // CAS 操作
context 是 Go 并发编程中用于传递取消信号、超时、截止时间和请求范围值的标准机制,在网络服务、API 调用链中无处不在。
import "context"
// 根 context
ctx := context.Background() // 通常作为顶层
ctx := context.TODO() // 不确定用哪个时的占位
// 可取消的 context
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 释放资源
// 带超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 带截止时间
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()
goroutine 通过 ctx.Done() channel 感知取消:
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信号:", ctx.Err())
return
default:
// 执行工作
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
worker(ctx) // 1 秒后自动取消
}
ctx := context.WithValue(context.Background(), "userID", 12345)
userID := ctx.Value("userID")
最佳实践:context 应作为函数的第一个参数显式传递(命名为
ctx),不要存储在结构体中;WithValue只用于传递请求范围的元数据(如 trace ID),不要用于传递可选参数。
数据竞争(data race)是并发编程中最隐蔽的 bug。Go 内置了强大的竞态检测器,在测试和运行时加上 -race 标志即可启用:
go run -race main.go
go test -race ./...
go build -race
竞态检测器会在运行时监控内存访问,一旦发现多个 goroutine 在没有同步的情况下并发访问同一内存(且至少有一个是写操作),就会报告详细的竞态信息。
强烈建议:在 CI 流程中始终启用
-race运行测试,及早发现并发问题。
本章讲解了 Go 并发的进阶工具:sync.Mutex/RWMutex 保护共享状态,WaitGroup 等待 goroutine 完成,Once 实现单次初始化,sync.Map 和 atomic 提供并发安全的数据访问,context 统一管理取消与超时。配合 -race 竞态检测器,可以构建出正确、高效的并发程序。channel 与锁各有适用场景,应根据问题选择最清晰的方案。
下一章我们将学习 Go 的包管理与 Go Modules。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。