sync.Mutex
Mutex 是「互斥锁」,用来保护一段代码或一块数据:同一时刻只有一个 goroutine 能持有这把锁,其他人想进这段临界区,就得等。
你会用到的 API
Lock():要进临界区时调用。若锁空闲,立刻拿到;若别人拿着,当前 goroutine 会阻塞,直到轮到它。Unlock():离开临界区时调用,把锁交给下一个等待者(如果有)。必须在曾经Lock成功的那条路径上配对调用,且不要重复Unlock未加锁的 mutex,否则会 panic。TryLock():试着拿锁,拿不到就立刻返回false,不会排队等。适合「能拿就拿,拿不到我就干别的事」的场景;在饥饿模式下运行时也可能拿不到(实现细节会保证公平性,TryLock 不参与那种交接)。
加锁时在干什么
- 常见情况:没人占锁时,
Lock()很快成功,开销相对小。 - 竞争激烈时:拿不到锁的 goroutine 会先短暂自旋(空转几圈看锁会不会马上释放),还不行就进等待队列并休眠,让出 CPU;被唤醒后再参与竞争。
- 原理:
- 正常模式:被唤醒的等待者要和新来的 goroutine 一起抢锁;新到的往往还在跑,更容易先抢到,极端情况下某个等待者可能一直轮不到(「饿死」风险)。
- 饥饿模式:若某个等待者等太久(实现里大约超过 1ms),运行时会把 mutex 切到饥饿模式——优先把锁交给排队最前面的等待者,新来的不再占便宜,避免尾延迟爆炸。队列变短或等待时间回落后,会再切回正常模式。
简单示例
var mu sync.Mutex
var counter int
func AddOne() {
mu.Lock()
defer mu.Unlock()
counter++
}
func TryAdd() bool {
if !mu.TryLock() {
return false // 拿不到锁,直接放弃
}
defer mu.Unlock()
counter++
return true
}
sync.RWMutex
在普通互斥锁基础上区分「读」和「写」——多个 goroutine 可以同时只读,但只要有人在写,读和写都不能同时进行。
适合读远多于写的场景(例如配置、缓存元数据频繁查、偶尔更新),读多时能比 Mutex 少很多阻塞。
你会用到的 API
RLock()/RUnlock():进入 / 离开只读临界区。只读时用这对,不要和写锁混用同一段逻辑时搞错配对。Lock()/Unlock():进入 / 离开写临界区(和Mutex语义一样,写时独占整把锁)。
读锁、写锁是同一把 RWMutex 上的两套操作,不能假设「我先 RLock 再在同一线程里 Lock 升级」——Go 里没有从读锁直接升级成写锁;需要写时,应先 RUnlock,再 Lock(中间可能被别人插入,这是设计如此,必要时要自己用额外逻辑保证一致性)。
和 Mutex 怎么选
- 读少写多或读写差不多:用
Mutex更简单,维护成本更低。 - 读非常多、写很少:
RWMutex可能更省等待时间;但实现比Mutex重,写锁会阻塞所有读者,若写频繁反而可能更差,需要结合实际压测。
简单示例
var rw sync.RWMutex
var data = map[string]int{"a": 1}
func Read(key string) int {
rw.RLock()
defer rw.RUnlock()
return data[key]
}
func Write(key string, v int) {
rw.Lock()
defer rw.Unlock()
data[key] = v
}
sync.WaitGroup
用来等一组 goroutine 全部跑完再往下执行
你会用到的 API
Add(delta int):把等待计数加上delta(常见是启动前Add(1)或Add(n))。一般在启动子任务之前调用,表示「还要等多个人」。Done():等价于Add(-1),表示一个子任务结束。通常在子 goroutine 里defer wg.Done(),避免漏减导致永远等不到。Wait():阻塞,直到计数变成 0。
典型用法
并发拉一批 RPC / HTTP、并行算子任务时:主流程先 Add,每个子任务 go func(){ defer Done(); ... }(),最后 Wait() 收束。Add 的总量要和 Done 次数对上,否则要么提前 Wait 返回,要么永远卡住。
简单示例
var wg sync.WaitGroup
urls := []string{"https://a", "https://b"}
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
_, _ = http.Get(url)
}(u)
}
wg.Wait() // 全部返回后再继续
errgroup(golang.org/x/sync/errgroup)
在 WaitGroup 思路上,帮你顺便管错误:一组任务里谁先出错可以记下来,并常配合 Context 取消让其余任务收手。
它内部会用 WaitGroup 等子任务结束,再用 sync.Once 只保留第一个错误,若创建时带了可取消的 context,出错后可以 cancel,避免无意义的 goroutine 空跑。
什么时候用
- 一批独立子任务,任意一个失败就想让整个批次失败,并取消兄弟任务。
- 比手写「每个 goroutine 往 err channel 塞 + WaitGroup」少样板代码。
简单示例
import (
"context"
"net/http"
"golang.org/x/sync/errgroup"
)
urls := []string{"https://a", "https://b"}
g, ctx := errgroup.WithContext(context.Background())
for _, url := range urls {
url := url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return resp.Body.Close()
})
}
if err := g.Wait(); err != nil {
// 第一个出错会走到这里,ctx 已被取消,其它请求会尽快结束
}
sync.Once
保证整个进程生命周期里,某段初始化逻辑只执行一次,多个 goroutine 同时调也安全。
你会用到的 API
Do(f func()):多次调用只会真正执行一次f,其余调用会等那一次跑完(适合懒加载单例、一次性注册)。
注意:Do 里如果 f panic,仍算作已执行过,再次 Do 不会重试——初始化代码要自己保证可重试或在外层处理。
简单示例
var once sync.Once
var cached string // 假设初始化很贵
func Expensive() string {
once.Do(func() {
cached = "..." // 只执行一次的真实初始化放这里
})
return cached
}
sync.Cond
条件变量——「不满足条件就睡觉,条件可能满足时被人叫醒」,用来协调**「等某个条件成立」**的 goroutine。
你会用到的 API
Wait():当前 goroutine 挂起,直到被Signal/Broadcast唤醒。调用前必须先持有与Cond关联的那把Mutex(已Lock);Wait内部会短暂解锁,被唤醒后再加锁返回,所以醒来时仍处在锁保护下。Signal():唤醒一个等待者(常用于「资源空出一个」)。Broadcast():唤醒所有等待者(常用于「条件全局变化」)。
在 Go 里很多场景用 channel 就能表达「等待 / 通知」,Cond 相对少用手写;但当多个 goroutine 等的是同一把锁 + 复杂条件、又不想拆成很多 channel 时,Cond 仍有用。不要用空 for {} 忙等,CPU 会飙;等条件应 Wait 让出执行权。
简单示例
var mu sync.Mutex
cond := sync.NewCond(&mu)
var ready bool
// 等待方
go func() {
mu.Lock()
for !ready {
cond.Wait() // 会暂时放开 mu,被唤醒后再持有 mu
}
mu.Unlock()
}()
// 通知方
mu.Lock()
ready = true
cond.Broadcast() // 或 Signal() 只叫醒一个
mu.Unlock()
Semaphore(golang.org/x/sync/semaphore)
带权重的信号量——同一时刻只允许「总共不超过 N 个单位」的并发(例如总并发 10,每个任务占 3 个单位,就类似「名额池」)。
你会用到的 API
Acquire(ctx, n):尝试占用n个名额;不够就阻塞,直到够或ctx取消。Release(n):归还n个名额,和Acquire成对使用。
适合限流、连接池式控制、和 context 超时/取消绑在一起的资源申请。
简单示例
import (
"context"
"golang.org/x/sync/semaphore"
)
// 同一时刻最多 10 个并发「名额」
var sem = semaphore.NewWeighted(10)
func handle(ctx context.Context) error {
if err := sem.Acquire(ctx, 1); err != nil {
return err // 可能 ctx 取消或超时
}
defer sem.Release(1)
// 执行业务…
return nil
}
singleflight(golang.org/x/sync/singleflight)
对同一个 key,并发多次「去算同一个结果」时,只真正执行一次,其余调用共享这一次的结果(或共享这一次的错误)。
典型场景:缓存击穿——同一 key 大量并发未命中,只让一个 goroutine 回源加载,其他人等它的结果,避免把数据库打爆。和「锁」不同:它解决的是重复工作合并,不是通用互斥临界区。
简单示例
import "golang.org/x/sync/singleflight"
var g singleflight.Group
func Load(key string) (string, error) {
v, err, shared := g.Do(key, func() (interface{}, error) {
// 同一 key 并发时,这里只会执行一次(例如回源 DB)
return "value-for-" + key, nil
})
if err != nil {
return "", err
}
_ = shared // true 表示复用了其它 goroutine 的那次调用
return v.(string), nil
}