Go 从 0 到精通 · 第 28 课:同步原语
学习定位:这是整套 Go 教程的第 28 课,也是阶段五(并发与工程阶段)的第四课。
前置要求:已经完成第 27 课,掌握了 select 与并发控制。需要理解 goroutine 和 channel 的基本使用。
本课目标:理解数据竞争问题,掌握 sync.Mutex、sync.RWMutex、sync.Once 等同步工具的使用,能用 -race 检测数据竞争,理解"什么时候用锁、什么时候用 channel"。
1. 本课你要解决的核心问题
前面三课你学了 goroutine、channel、select,已经能写不少并发程序了。但有一类问题还没解决:
1 2 3 4 5 6 7 counter := 0 for i := 0 ; i < 1000 ; i++ { go func () { counter++ }() }
你觉得最终 counter 是 1000?不一定。可能是 987,可能是 952,每次运行结果都不同。这就是数据竞争(data race) 。
你需要搞明白以下问题:
什么是数据竞争,为什么会发生
怎么用 go run -race 检测数据竞争
sync.Mutex 怎么用——互斥锁
sync.RWMutex 怎么用——读写锁
sync.Once 怎么用——只执行一次
sync.WaitGroup 的进阶用法(第 25 课初步介绍过)
什么时候用锁,什么时候用 channel
2. 数据竞争——并发编程的头号敌人
2.1 看一个 bug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "sync" ) func main () { counter := 0 var wg sync.WaitGroup for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) go func () { defer wg.Done() counter++ }() } wg.Wait() fmt.Println("counter =" , counter) }
运行多次,你可能看到不同的结果:
1 2 3 4 counter = 962 counter = 987 counter = 1000 counter = 951
2.2 为什么结果不对
counter++ 看起来是一步操作,实际上是三步:
1 2 3 1 . 读取 counter 的值(比如 42 )2 . 加 1 (得到 43 )3 . 写回 counter
当两个 goroutine 同时执行 counter++ 时:
1 2 3 4 goroutine A: 读取 counter = 42 goroutine B: 读取 counter = 42 ← 读到了同样的值goroutine A: 写入 counter = 43 goroutine B: 写入 counter = 43 ← 覆盖了 A 的结果
两个 goroutine 各自加了 1,但 counter 只从 42 变到了 43,丢了一次。这就是数据竞争 。
2.3 用 -race 检测
Go 内置了数据竞争检测器。只需要在运行或编译时加 -race 标志:
输出会包含类似这样的警告:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ================== WARNING: DATA RACE Read at 0x00c0000b4010 by goroutine 7: main.main.func1() /path/main.go:14 +0x38 Previous write at 0x00c0000b4010 by goroutine 6: main.main.func1() /path/main.go:14 +0x4e Goroutine 7 (running) created at: main.main() /path/main.go:12 +0x6c ==================
它会精确告诉你哪一行代码存在数据竞争。养成习惯:开发和测试阶段加 -race 。
1 2 go test -race ./... go build -race
3. sync.Mutex——互斥锁
3.1 什么是互斥锁
互斥锁保证同一时间只有一个 goroutine 能访问被保护的资源。
就像公共厕所的门锁:进去之前锁门(Lock),出来之后开锁(Unlock)。别人想进去就得等门开了才行。
3.2 修复数据竞争
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "sync" ) func main () { counter := 0 var mu sync.Mutex var wg sync.WaitGroup for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) go func () { defer wg.Done() mu.Lock() counter++ mu.Unlock() }() } wg.Wait() fmt.Println("counter =" , counter) }
现在用 go run -race main.go 运行,不会再有竞争警告,结果每次都是 1000。
3.3 Lock 和 Unlock 的规则
1 2 Lock () → 获取锁。如果锁已被占用,当前 goroutine 阻塞等待。Unlock () → 释放锁。如果没有持有锁就 Unlock,会 panic。
3.4 用 defer 确保解锁
1 2 3 4 5 6 7 8 mu.Lock() defer mu.Unlock() if condition { return } doSomething()
永远用 defer mu.Unlock() 。如果忘了解锁,其他 goroutine 会永远等下去(死锁)。
3.5 把锁和它保护的数据放在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.count++ } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.count } func main () { counter := &SafeCounter{} var wg sync.WaitGroup for i := 0 ; i < 1000 ; i++ { wg.Add(1 ) go func () { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Println("counter =" , counter.Value()) }
把锁和数据绑在一起,让使用者不需要关心加锁解锁,只需要调用方法。锁是数据的保镖,要跟着数据走。
3.6 并发安全的 map
Go 的 map 不是并发安全的。多个 goroutine 同时读写 map 会直接 panic:
1 2 3 4 5 m := make (map [string ]int ) go func () { m["a" ] = 1 }()go func () { m["b" ] = 2 }()
用 Mutex 保护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type SafeMap struct { mu sync.Mutex data map [string ]int } func NewSafeMap () *SafeMap { return &SafeMap{data: make (map [string ]int )} } func (m *SafeMap) Set(key string , value int ) { m.mu.Lock() defer m.mu.Unlock() m.data[key] = value } func (m *SafeMap) Get(key string ) (int , bool ) { m.mu.Lock() defer m.mu.Unlock() v, ok := m.data[key] return v, ok } func (m *SafeMap) Delete(key string ) { m.mu.Lock() defer m.mu.Unlock() delete (m.data, key) }
4. sync.RWMutex——读写锁
4.1 问题:读多写少的场景
普通 Mutex 有个缺点:不管读还是写,都要排队。但很多时候,读操作远多于写操作(比如缓存)。多个读操作之间不会冲突,不需要互斥。
4.2 RWMutex 的规则
sync.RWMutex 区分读锁和写锁:
操作
方法
规则
读锁
RLock() / RUnlock()
多个读可以同时持有
写锁
Lock() / Unlock()
独占,读和写都要等
1 2 3 多个读锁可以共存 → 读不阻塞读 写锁和读锁互斥 → 写阻塞读,读也阻塞写 写锁和写锁互斥 → 写阻塞写
4.3 使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package mainimport ( "fmt" "sync" "time" ) type Cache struct { mu sync.RWMutex data map [string ]string } func NewCache () *Cache { return &Cache{data: make (map [string ]string )} } func (c *Cache) Get(key string ) (string , bool ) { c.mu.RLock() defer c.mu.RUnlock() v, ok := c.data[key] return v, ok } func (c *Cache) Set(key, value string ) { c.mu.Lock() defer c.mu.Unlock() c.data[key] = value } func main () { cache := NewCache() cache.Set("name" , "Go" ) var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() for j := 0 ; j < 100 ; j++ { if v, ok := cache.Get("name" ); ok { _ = v } } fmt.Printf("读者 %d 完成\n" , id) }(i) } for i := 0 ; i < 2 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() for j := 0 ; j < 10 ; j++ { cache.Set("name" , fmt.Sprintf("Go_%d_%d" , id, j)) time.Sleep(10 * time.Millisecond) } fmt.Printf("写者 %d 完成\n" , id) }(i) } wg.Wait() fmt.Println("完成" ) }
10 个读 goroutine 可以同时读,互不阻塞。只有写操作会独占锁。
4.4 什么时候用 RWMutex
读操作远多于写操作 → 用 RWMutex(读不互斥,性能更好)
读写频率差不多 → 用 Mutex(RWMutex 本身有额外开销)
只有写操作 → 用 Mutex
简单规则:不确定就用 Mutex,确认读多写少再换 RWMutex。
5. sync.Once——只执行一次
5.1 场景:初始化只做一次
有些操作只应该执行一次,比如初始化数据库连接、加载配置文件。即使多个 goroutine 同时调用,也只执行第一次。
5.2 基本用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "fmt" "sync" ) var once sync.Oncefunc initialize () { fmt.Println("初始化...(只会打印一次)" ) } func worker (id int , wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("Worker %d 尝试初始化\n" , id) once.Do(initialize) fmt.Printf("Worker %d 继续工作\n" , id) } func main () { var wg sync.WaitGroup for i := 1 ; i <= 5 ; i++ { wg.Add(1 ) go worker(i, &wg) } wg.Wait() }
可能的输出:
1 2 3 4 5 6 7 8 9 10 11 Worker 3 尝试初始化 初始化...(只会打印一次) Worker 3 继续工作 Worker 1 尝试初始化 Worker 1 继续工作 Worker 5 尝试初始化 Worker 5 继续工作 Worker 2 尝试初始化 Worker 2 继续工作 Worker 4 尝试初始化 Worker 4 继续工作
“初始化…” 只打印了一次。其他 goroutine 调用 once.Do 时会等第一次执行完毕后直接返回。
5.3 单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type Database struct { host string } var ( dbInstance *Database dbOnce sync.Once ) func GetDB () *Database { dbOnce.Do(func () { fmt.Println("创建数据库连接" ) dbInstance = &Database{host: "localhost:5432" } }) return dbInstance } func main () { var wg sync.WaitGroup for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func () { defer wg.Done() db := GetDB() fmt.Println("使用数据库:" , db.host) }() } wg.Wait() }
不管多少个 goroutine 调用 GetDB(),数据库连接只会创建一次。
5.4 Once 的特性
Do 方法接收一个无参数无返回值的函数
只执行第一次调用传入的函数,后续调用直接返回
即使第一次执行的函数 panic 了,后续调用也不会再执行(认为"已经执行过了")
一个 Once 只能配一个操作,不能重置
6. sync.WaitGroup 进阶
第 25 课初步介绍了 WaitGroup。这里补充几个进阶用法。
6.1 一次性 Add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var wg sync.WaitGroupn := 10 for i := 0 ; i < n; i++ { wg.Add(1 ) go worker(i, &wg) } wg.Add(n) for i := 0 ; i < n; i++ { go worker(i, &wg) }
两种都可以。方式二更直观——一眼就能看出总共有 n 个任务。但要确保 Add 的数量和实际启动的 goroutine 数量一致。
6.2 嵌套 WaitGroup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "sync" ) func main () { var outerWg sync.WaitGroup for stage := 1 ; stage <= 3 ; stage++ { outerWg.Add(1 ) go func (s int ) { defer outerWg.Done() var innerWg sync.WaitGroup for task := 1 ; task <= 3 ; task++ { innerWg.Add(1 ) go func (t int ) { defer innerWg.Done() fmt.Printf("阶段 %d - 任务 %d 完成\n" , s, t) }(task) } innerWg.Wait() fmt.Printf("=== 阶段 %d 全部完成 ===\n" , s) }(stage) } outerWg.Wait() fmt.Println("所有阶段完成" ) }
外层 WaitGroup 等所有阶段完成,每个阶段内部有自己的 WaitGroup 等所有任务完成。
7. sync.Map——并发安全的 map
7.1 标准库提供的并发 map
Go 标准库提供了 sync.Map,不需要自己加锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "sync" ) func main () { var m sync.Map var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func (n int ) { defer wg.Done() m.Store(fmt.Sprintf("key_%d" , n), n*n) }(i) } wg.Wait() m.Range(func (key, value any) bool { fmt.Printf("%s = %v\n" , key, value) return true }) if v, ok := m.Load("key_5" ); ok { fmt.Println("key_5 =" , v) } m.Delete("key_3" ) actual, loaded := m.LoadOrStore("key_5" , 999 ) fmt.Println("key_5:" , actual, "已存在:" , loaded) }
7.2 sync.Map 的 API
方法
作用
Store(key, value)
存入键值对
Load(key)
读取,返回 (value, ok)
Delete(key)
删除
LoadOrStore(key, value)
存在返回旧值,不存在存入新值
LoadAndDelete(key)
读取并删除
Range(func)
遍历所有键值对
7.3 sync.Map vs 自己加锁的 map
特性
sync.Map
Mutex + map
使用难度
简单,不用管锁
需要自己加锁解锁
类型安全
不安全(key 和 value 都是 any)
安全(有具体类型)
适用场景
key 相对固定、读多写少
通用场景
性能
特定场景更好
一般场景更稳定
选择建议 :
大多数情况用 Mutex + 普通 map(类型安全、更灵活)
两种特殊场景用 sync.Map:key 只增不删、各 goroutine 读写不同的 key
8. 锁 vs channel——怎么选
这是 Go 并发编程中最常见的疑问。
8.1 用 channel 的场景
goroutine 之间传递数据
协调多个 goroutine 的执行顺序
流水线、生产者-消费者模式
通知和信号
1 2 3 4 results := make (chan Result) go func () { results <- compute() }()r := <-results
8.2 用锁的场景
保护共享状态 (计数器、缓存、连接池)
多个 goroutine 读写同一个数据结构
简单的互斥访问
1 2 3 4 mu.Lock() counter++ mu.Unlock()
8.3 一句话总结
传递数据用 channel,保护数据用锁。
场景
选择
原因
goroutine 间传递数据
channel
数据在"流动"
保护共享的计数器
Mutex
数据在"原地修改"
保护共享的 map
Mutex / sync.Map
数据在"原地修改"
通知某件事完成
channel
这是一种通信
只执行一次初始化
sync.Once
专用工具
等待一组 goroutine
WaitGroup
专用工具
9. 实战示例
9.1 并发安全的银行账户
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package mainimport ( "fmt" "sync" ) type Account struct { mu sync.Mutex balance int } func NewAccount (initial int ) *Account { return &Account{balance: initial} } func (a *Account) Deposit(amount int ) { a.mu.Lock() defer a.mu.Unlock() a.balance += amount fmt.Printf(" 存入 %d, 余额: %d\n" , amount, a.balance) } func (a *Account) Withdraw(amount int ) bool { a.mu.Lock() defer a.mu.Unlock() if a.balance < amount { fmt.Printf(" 取款 %d 失败,余额不足: %d\n" , amount, a.balance) return false } a.balance -= amount fmt.Printf(" 取出 %d, 余额: %d\n" , amount, a.balance) return true } func (a *Account) Balance() int { a.mu.Lock() defer a.mu.Unlock() return a.balance } func main () { account := NewAccount(1000 ) var wg sync.WaitGroup operations := []struct { op string amount int }{ {"deposit" , 200 }, {"withdraw" , 500 }, {"deposit" , 300 }, {"withdraw" , 800 }, {"withdraw" , 100 }, {"deposit" , 150 }, } for _, op := range operations { wg.Add(1 ) go func (action string , amount int ) { defer wg.Done() switch action { case "deposit" : account.Deposit(amount) case "withdraw" : account.Withdraw(amount) } }(op.op, op.amount) } wg.Wait() fmt.Printf("\n最终余额: %d\n" , account.Balance()) }
每个操作都加了锁,保证余额计算不会被打乱。
9.2 并发安全的访问计数器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package mainimport ( "fmt" "sync" "time" ) type PageCounter struct { mu sync.RWMutex counts map [string ]int } func NewPageCounter () *PageCounter { return &PageCounter{counts: make (map [string ]int )} } func (pc *PageCounter) Visit(page string ) { pc.mu.Lock() defer pc.mu.Unlock() pc.counts[page]++ } func (pc *PageCounter) GetCount(page string ) int { pc.mu.RLock() defer pc.mu.RUnlock() return pc.counts[page] } func (pc *PageCounter) TopPages(n int ) []string { pc.mu.RLock() defer pc.mu.RUnlock() type kv struct { page string count int } var pages []kv for p, c := range pc.counts { pages = append (pages, kv{p, c}) } for i := 0 ; i < len (pages)-1 ; i++ { for j := i + 1 ; j < len (pages); j++ { if pages[j].count > pages[i].count { pages[i], pages[j] = pages[j], pages[i] } } } result := []string {} for i := 0 ; i < n && i < len (pages); i++ { result = append (result, fmt.Sprintf("%s: %d次" , pages[i].page, pages[i].count)) } return result } func main () { counter := NewPageCounter() pages := []string {"/home" , "/about" , "/api/users" , "/home" , "/products" , "/home" , "/about" } var wg sync.WaitGroup for i := 0 ; i < 100 ; i++ { wg.Add(1 ) go func () { defer wg.Done() page := pages[i%len (pages)] counter.Visit(page) }( ) } wg.Wait() for _, line := range counter.TopPages(5 ) { fmt.Println(line) } }
等一下——上面的代码有闭包陷阱(第 25 课)。让我用正确的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { counter := NewPageCounter() pages := []string {"/home" , "/about" , "/api/users" , "/home" , "/products" , "/home" , "/about" } var wg sync.WaitGroup for i := 0 ; i < 100 ; i++ { wg.Add(1 ) go func (idx int ) { defer wg.Done() page := pages[idx%len (pages)] counter.Visit(page) time.Sleep(time.Millisecond) }(i) } wg.Wait() fmt.Println("=== 页面访问排行 ===" ) for _, line := range counter.TopPages(5 ) { fmt.Println(line) } }
用了 RWMutex:Visit 用写锁(修改数据),GetCount 和 TopPages 用读锁(只读数据)。多个读请求可以同时执行。
9.3 用 Once 实现懒加载配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package mainimport ( "encoding/json" "fmt" "os" "sync" ) type Config struct { AppName string `json:"app_name"` Port int `json:"port"` Debug bool `json:"debug"` } var ( config *Config configOnce sync.Once configErr error ) func GetConfig () (*Config, error ) { configOnce.Do(func () { fmt.Println("加载配置文件...(只执行一次)" ) data, err := os.ReadFile("config.json" ) if err != nil { configErr = fmt.Errorf("读取配置失败: %w" , err) return } config = &Config{} if err := json.Unmarshal(data, config); err != nil { configErr = fmt.Errorf("解析配置失败: %w" , err) config = nil } }) return config, configErr } func main () { var wg sync.WaitGroup for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() cfg, err := GetConfig() if err != nil { fmt.Printf("Worker %d: 配置加载失败: %v\n" , id, err) return } fmt.Printf("Worker %d: 使用配置 %s:%d\n" , id, cfg.AppName, cfg.Port) }(i) } wg.Wait() }
配置只加载一次,后续所有调用直接返回缓存的结果。
10. 常见坑总结
10.1 忘记解锁——死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func bad () { mu.Lock() if someCondition { return } mu.Unlock() } func good () { mu.Lock() defer mu.Unlock() if someCondition { return } }
10.2 重复加锁——死锁
1 2 3 4 func doublelock () { mu.Lock() mu.Lock() }
Go 的 sync.Mutex 不是可重入锁。同一个 goroutine 对同一个 Mutex 加锁两次会死锁。
1 2 3 4 5 6 7 8 9 10 11 12 func (s *SafeData) A() { s.mu.Lock() defer s.mu.Unlock() s.B() } func (s *SafeData) B() { s.mu.Lock() defer s.mu.Unlock() }
解决 :提取一个不加锁的内部方法,让公开方法加锁后调用内部方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (s *SafeData) A() { s.mu.Lock() defer s.mu.Unlock() s.b() } func (s *SafeData) B() { s.mu.Lock() defer s.mu.Unlock() s.b() } func (s *SafeData) b() { }
10.3 锁的粒度太大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func bad (mu *sync.Mutex) { mu.Lock() defer mu.Unlock() data := fetchFromNetwork() result := processData(data) saveResult(result) } func good (mu *sync.Mutex) { data := fetchFromNetwork() result := processData(data) mu.Lock() saveResult(result) mu.Unlock() }
锁的持有时间越短越好。 持有锁的时候做耗时操作,会让其他 goroutine 白白等待。
10.4 复制 Mutex
1 2 3 4 5 6 7 8 9 10 type Data struct { mu sync.Mutex v int } func modify (d Data) { d.mu.Lock() defer d.mu.Unlock() d.v++ }
sync.Mutex、sync.WaitGroup 等同步原语不能被复制 。如果包含在结构体中,结构体也不能被值传递,必须用指针。
1 2 3 4 5 func modify (d *Data) { d.mu.Lock() defer d.mu.Unlock() d.v++ }
10.5 读写锁中用错了锁类型
1 2 3 4 5 6 func (c *Cache) Set(key, value string ) { c.mu.RLock() defer c.mu.RUnlock() c.data[key] = value }
读操作用 RLock/RUnlock,写操作用 Lock/Unlock。搞反了就失去了保护。
11. 本课练习
练习 1:并发安全的计数器
要求:
实现一个 Counter 结构体,支持 Add(n)、Sub(n)、Value() 方法
所有方法并发安全
启动 100 个 goroutine 各 Add(1),再启动 50 个各 Sub(1)
最终值应该是 50
练习 2:并发安全的排行榜
要求:
实现一个 Leaderboard 结构体,支持 AddScore(name, score) 和 Top(n) 方法
AddScore 累加分数
Top(n) 返回前 n 名(按分数降序)
用 RWMutex 保护:AddScore 用写锁,Top 用读锁
启动多个 goroutine 并发添加分数和查询排名
练习 3:限流器
要求:
实现一个 RateLimiter 结构体,限制每秒最多处理 N 个请求
用 Mutex + 时间戳数组实现
Allow() 方法返回 bool,表示当前请求是否被允许
提示:记录最近 N 个请求的时间戳,新请求来时检查最早那个是否在 1 秒内。
练习 4:数据竞争检测练习
要求:
写一段故意有数据竞争 的代码
用 go run -race 运行,观察输出
用 Mutex 修复,再用 -race 确认修复成功
练习 5:读写锁性能对比
要求:
分别用 Mutex 和 RWMutex 保护一个 map
模拟 90% 读 + 10% 写的场景
用 time.Since 测量两种方式的耗时
对比结果
12. 自测题
12.1 概念题
什么是数据竞争?counter++ 为什么不是并发安全的?
go run -race 做了什么?应该在什么阶段使用?
sync.Mutex 的 Lock 和 Unlock 分别做什么?
为什么总应该用 defer mu.Unlock() 而不是手动调用 Unlock()?
sync.RWMutex 和 sync.Mutex 有什么区别?什么场景用 RWMutex?
sync.Once 的 Do 方法被调用多次会怎样?
Go 的 sync.Mutex 是可重入的吗?同一个 goroutine 连续 Lock 两次会怎样?
"传递数据用 channel,保护数据用锁"这句话怎么理解?
12.2 代码阅读题
预测以下代码是否存在数据竞争:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "fmt" "sync" ) func main () { data := make (map [string ]int ) var mu sync.Mutex var wg sync.WaitGroup for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func (n int ) { defer wg.Done() key := fmt.Sprintf("key_%d" , n) mu.Lock() data[key] = n * n mu.Unlock() }(i) } wg.Wait() for k, v := range data { fmt.Println(k, v) } }
点击查看答案
没有数据竞争。
解释:
所有对 data 的写操作都在 mu.Lock() 和 mu.Unlock() 之间——受锁保护
wg.Wait() 确保所有写操作完成后才开始读(for range)
主 goroutine 中的 for range 是在所有 goroutine 结束后执行的,此时没有并发写入
闭包变量 i 通过参数 n 传入,每个 goroutine 有自己的副本
这段代码是安全的。用 go run -race 运行不会报任何警告。
13. 本课总结
这一课你学到了如何在并发环境中保护共享数据。
工具
用途
关键点
sync.Mutex
互斥锁
同一时间只有一个 goroutine 能访问
sync.RWMutex
读写锁
多读不互斥,写独占
sync.Once
只执行一次
初始化、单例
sync.WaitGroup
等待一组 goroutine
Add → Done → Wait
sync.Map
并发安全 map
API 是 any 类型,少数场景适用
-race
数据竞争检测
开发测试阶段必用
最重要的三件事:
多个 goroutine 读写同一个变量就有数据竞争——用 Mutex 保护,用 -race 检测
defer mu.Unlock() 是铁律——防止忘记解锁导致死锁
传递数据用 channel,保护数据用锁——两者互补,不要只用一种
14. 下一课预告
你现在能创建 goroutine、用 channel 通信、用 select 多路复用、用锁保护共享数据。但还有一个重要的问题:怎么取消一个正在运行的 goroutine?
想象一个场景:你发起了一个 HTTP 请求,但用户取消了操作。请求的 goroutine 还在傻傻等待响应。你需要一种机制告诉它"不用干了,回来吧"。
下一课:context 与取消机制
会重点讲:
context 是什么——goroutine 的生命周期管理器
context.Background() 和 context.TODO()
context.WithCancel——手动取消
context.WithTimeout 和 context.WithDeadline——自动超时取消
怎么在 goroutine 中监听取消信号
context 的传递约定
学完下一课,你就能优雅地管理 goroutine 的生命周期了。