Go sync 包详解:并发同步原语与高效协作模式

深入讲解 Go sync 包的 Mutex、RWMutex、WaitGroup、Once、Pool 等并发工具的内部原理与实现,涵盖限流器、资源池与高并发缓存等实战案例,以及锁粒度控制与死锁避免的最佳实践

sync 包:Go 的同步工具箱

前两篇文章我们学了 goroutine 和 channel——Go 并发编程的两大利器。但现实中,不是所有并发问题都适合用 channel 来解决。

有时候你只需要保护一小段临界区代码,用 channel 反而显得笨重。这时候,传统的同步原语——互斥锁、读写锁、等待组等——就是你的好朋友。

Go 的 sync 包提供了这些工具。它虽然不大,但每个工具都精心设计,覆盖了并发编程中最常见的场景。今天我们就来逐个拆解这个工具箱。

sync.Mutex:互斥锁

为什么需要互斥锁?

先看一个问题:

package main

import (
	"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)
}

你觉得这段代码的输出是多少?1000?

不,每次运行的结果都可能不同,而且几乎不会是 1000。可能是 987、993、976……

这是因为 counter++ 不是一个原子操作。它实际上包含三个步骤:

  1. 读取 counter 的当前值
  2. 把值加 1
  3. 写回 counter

当多个 goroutine 同时执行这三步时,就会发生竞态条件(race condition):

Goroutine A: 读取 counter = 100
Goroutine B: 读取 counter = 100   ← 读到了相同的旧值
Goroutine A: 写入 counter = 101
Goroutine B: 写入 counter = 101   ← 覆盖了 A 的结果!

两次 counter++,但 counter 只增加了 1。这就是并发编程中最经典的 bug。

用 Mutex 保护临界区

sync.Mutex 提供两个方法:

  • Lock():获取锁。如果锁已被其他 goroutine 持有,会阻塞等待。
  • Unlock():释放锁。
package main

import (
	"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)  // 1000(总是正确的)
}

现在 counter++ 被锁保护起来了。同一时刻只有一个 goroutine 能执行这段代码,结果就正确了。

用 defer 确保释放

实际开发中,推荐用 defer 来确保锁一定会被释放:

mu.Lock()
defer mu.Unlock()
// 临界区代码...

即使临界区代码发生 panic,defer 也会确保锁被释放。否则,其他 goroutine 会永远等待——这就是死锁。

封装成线程安全的结构体

把 Mutex 和它保护的数据封装在一起是一个好习惯:

type SafeCounter struct {
	mu    sync.Mutex
	value int
}

func (c *SafeCounter) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.value++
}

func (c *SafeCounter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.value
}

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.Value())  // 1000
}

⚠️ 重要:sync.Mutex 不能被复制。如果你需要传递包含 Mutex 的结构体,传指针而不是值:

// ❌ 不好:按值传递会复制 Mutex
func process(c SafeCounter) { ... }

// ✅ 好:按指针传递
func process(c *SafeCounter) { ... }

sync.RWMutex:读写锁

sync.RWMutex 是 Mutex 的增强版。它区分读锁和写锁:

  • 读锁(共享锁):多个 goroutine 可以同时持有读锁
  • 写锁(排他锁):只有一个 goroutine 能持有写锁,且与读锁互斥

这非常适合读多写少的场景:

type Cache struct {
	mu    sync.RWMutex
	items map[string]string
}

func NewCache() *Cache {
	return &Cache{
		items: make(map[string]string),
	}
}

// Get 读取数据(使用读锁)
func (c *Cache) Get(key string) (string, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	value, ok := c.items[key]
	return value, ok
}

// Set 写入数据(使用写锁)
func (c *Cache) Set(key, value string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.items[key] = value
}

func main() {
	cache := NewCache()

	// 多个 goroutine 同时读取
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			cache.Get(fmt.Sprintf("key%d", i))
		}(i)
	}

	// 一些 goroutine 同时写入
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
		}(i)
	}

	wg.Wait()
}

读写锁的规则

操作互斥
读锁 vs 读锁❌ 不互斥(可以并发读)
读锁 vs 写锁✅ 互斥
写锁 vs 写锁✅ 互斥

简单说:读读并发,读写互斥,写写互斥。

性能对比

让我们看看读写锁和互斥锁在读多写少场景下的性能差异:

package main

import (
	"sync"
	"testing"
)

type MutexMap struct {
	mu   sync.Mutex
	data map[string]int
}

func (m *MutexMap) Get(key string) int {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.data[key]
}

type RWMap struct {
	mu   sync.RWMutex
	data map[string]int
}

func (m *RWMap) Get(key string) int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.data[key]
}

func BenchmarkMutexGet(b *testing.B) {
	m := &MutexMap{data: map[string]int{"a": 1}}
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			m.Get("a")
		}
	})
}

func BenchmarkRWMapGet(b *testing.B) {
	m := &RWMap{data: map[string]int{"a": 1}}
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			m.Get("a")
		}
	})
}

在高并发读的场景下,读写锁的性能可以比互斥锁快几倍甚至几十倍。

sync.WaitGroup:等待一组 goroutine

sync.WaitGroup 是我们已经用过好几次的老朋友了。它用来等待一组 goroutine 完成工作。

三个方法:

  • Add(delta int):增加计数
  • Done():减少计数(等价于 Add(-1))
  • Wait():阻塞,直到计数变为 0
func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)  // 增加计数
		go func(id int) {
			defer wg.Done()  // 确保计数减 1
			fmt.Printf("Task %d 开始\n", id)
			time.Sleep(time.Second)
			fmt.Printf("Task %d 完成\n", id)
		}(i)
	}

	fmt.Println("等待所有任务完成...")
	wg.Wait()
	fmt.Println("所有任务已完成!")
}

⚠️ 注意事项:

  1. Add 必须在 goroutine 启动之前调用,否则可能和 Wait 竞态
  2. Done 最好用 defer 调用,确保一定会执行
  3. 不要把 WaitGroup 传给 goroutine 后,在外部再调用 Add

sync.Once:只执行一次

sync.Once 确保某段代码只会被执行一次,即使在多个 goroutine 中并发调用。

典型用途是延迟初始化(lazy initialization):

package main

import (
	"fmt"
	"sync"
)

type Config struct {
	DatabaseURL string
	Port        int
}

var (
	config *Config
	once   sync.Once
)

func loadConfig() *Config {
	once.Do(func() {
		fmt.Println("加载配置文件...")  // 这行只会打印一次
		config = &Config{
			DatabaseURL: "postgres://localhost/mydb",
			Port:        8080,
		}
	})
	return config
}

func main() {
	var wg sync.WaitGroup

	// 多个 goroutine 同时请求配置
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			cfg := loadConfig()
			fmt.Printf("Goroutine %d: port = %d\n", id, cfg.Port)
		}(i)
	}

	wg.Wait()
}

输出:

加载配置文件...
Goroutine 0: port = 8080
Goroutine 1: port = 8080
...

“加载配置文件…“只打印了一次。sync.Once 保证了初始化的唯一性。

实现单例模式

sync.Once 是 Go 中实现单例模式的标准方式:

type Database struct {
	conn *sql.DB
}

var (
	instance *Database
	once     sync.Once
)

func GetDatabase() *Database {
	once.Do(func() {
		db, err := sql.Open("postgres", "dsn")
		if err != nil {
			panic(err)
		}
		instance = &Database{conn: db}
	})
	return instance
}

sync.Pool:对象池

sync.Pool 是一个临时对象的池子,用来缓存和复用频繁创建和销毁的对象,减少 GC 压力。

package main

import (
	"bytes"
	"fmt"
	"sync"
)

// 创建一个 Buffer 池
var bufPool = sync.Pool{
	New: func() interface{} {
		return new(bytes.Buffer)
	},
}

func processRequest(data string) {
	// 从池中获取一个 Buffer
	buf := bufPool.Get().(*bytes.Buffer)
	
	// 使用完毕后清空并放回池中
	buf.Reset()
	buf.WriteString(data)
	fmt.Printf("处理: %s (len=%d)\n", buf.String(), buf.Len())
	
	bufPool.Put(buf)
}

func main() {
	for i := 0; i < 5; i++ {
		processRequest(fmt.Sprintf("request-%d", i))
	}
}

sync.Pool 的关键方法:

  • Get():从池中获取一个对象(如果池为空,调用 New 创建新的)
  • Put(obj):把对象放回池中

⚠️ 注意:池中的对象会在 GC 时被自动清理。所以 sync.Pool 适合缓存短生命周期的临时对象,不适合做长期缓存。

sync.Pool 在 Go 标准库中被广泛使用,比如 fmt 包就用它来缓存打印时用到的缓冲区。

sync.Map:并发安全的 Map

在之前的文章中我们知道,普通 map 不是并发安全的。sync.Map 是 Go 1.9 引入的并发安全版本。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 存储
	m.Store("name", "张三")
	m.Store("age", 25)

	// 读取
	if value, ok := m.Load("name"); ok {
		fmt.Println("name:", value)
	}

	// 读取或存储(如果 key 不存在则存储)
	value, loaded := m.LoadOrStore("city", "北京")
	fmt.Printf("city: %v (loaded: %v)\n", value, loaded)

	// 删除
	m.Delete("age")

	// 遍历
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("%v: %v\n", key, value)
		return true  // 返回 false 停止遍历
	})
}

sync.Map 适合以下场景:

  • 读多写少:多个 goroutine 读取,偶尔有写入
  • 键空间不重叠:不同的 goroutine 操作不同的键

对于其他场景,用 sync.RWMutex + 普通 map 通常性能更好。

条件变量:sync.Cond

sync.Cond 用来协调多个 goroutine 之间的等待和通知。它比 channel 更灵活,但使用也更复杂。

package main

import (
	"fmt"
	"sync"
	"time"
)

type Queue struct {
	mu       sync.Mutex
	cond     *sync.Cond
	items    []string
	maxSize  int
}

func NewQueue(maxSize int) *Queue {
	q := &Queue{
		items:   make([]string, 0),
		maxSize: maxSize,
	}
	q.cond = sync.NewCond(&q.mu)
	return q
}

func (q *Queue) Push(item string) {
	q.mu.Lock()
	defer q.mu.Unlock()

	// 等待队列有空间
	for len(q.items) >= q.maxSize {
		q.cond.Wait()  // 阻塞等待,同时释放锁
	}

	q.items = append(q.items, item)
	fmt.Printf("入队: %s (size: %d)\n", item, len(q.items))
	q.cond.Signal()  // 通知等待的消费者
}

func (q *Queue) Pop() string {
	q.mu.Lock()
	defer q.mu.Unlock()

	// 等待队列有数据
	for len(q.items) == 0 {
		q.cond.Wait()
	}

	item := q.items[0]
	q.items = q.items[1:]
	fmt.Printf("出队: %s (size: %d)\n", item, len(q.items))
	q.cond.Signal()  // 通知等待的生产者
	return item
}

func main() {
	q := NewQueue(3)
	var wg sync.WaitGroup

	// 启动生产者
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 1; i <= 10; i++ {
			q.Push(fmt.Sprintf("item-%d", i))
			time.Sleep(100 * time.Millisecond)
		}
	}()

	// 启动消费者
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			time.Sleep(300 * time.Millisecond)
			q.Pop()
		}
	}()

	wg.Wait()
}

sync.Cond 在实际开发中用得不多,因为大多数场景用 channel 更简洁。但了解它的存在还是很有用的。

锁 vs Channel:如何选择?

这是很多 Go 开发者纠结的问题。这里给出一些指导原则:

用 Channel 的场景

  • 传递数据的所有权:一个 goroutine 生产数据,另一个消费
  • 协调多个 goroutine 的执行顺序
  • 超时和取消操作
  • 分发任务给多个 worker

用锁的场景

  • 保护内部状态:比如缓存、计数器
  • 细粒度的锁操作:只锁住几行代码
  • 性能敏感的临界区:锁的开销比 channel 小

一个简单的原则

用 channel 做 goroutine 之间的通信,用锁做 goroutine 内部的同步。

// ✅ 好:用锁保护内部状态
type Cache struct {
	mu    sync.RWMutex
	items map[string]string
}

// ✅ 好:用 channel 在 goroutine 之间传递数据
func process(jobs <-chan Job, results chan<- Result) {
	for job := range jobs {
		results <- doWork(job)
	}
}

用 -race 检测竞态条件

Go 提供了一个强大的工具:竞态检测器(race detector)。

go run -race main.go
go test -race ./...
go build -race

它能在运行时检测出数据竞态,即使你的程序"看起来工作正常"也能发现问题。

// race_test.go
package main

import (
	"sync"
	"testing"
)

func TestRace(t *testing.T) {
	data := make(map[string]int)
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			key := fmt.Sprintf("key%d", i)
			data[key] = i  // 竞态条件!
		}(i)
	}

	wg.Wait()
}

运行 go test -race 会报告竞态条件的位置。

⚠️ 最佳实践:在 CI/CD 中加入 go test -race,确保每次提交都没有竞态条件。

小结

今天我们全面学习了 sync 包:

  1. Mutex:互斥锁,保护临界区代码
  2. RWMutex:读写锁,读多写少场景性能更好
  3. WaitGroup:等待一组 goroutine 完成
  4. Once:确保代码只执行一次
  5. Pool:对象池,减少 GC 压力
  6. Map:并发安全的 map
  7. Cond:条件变量,复杂场景下的协调工具

最后,记住一个重要的原则:能用 channel 解决的,就不用锁;能用锁解决的,就不用 channel。选择最合适的工具,而不是最复杂的。

练习时间

  1. 安全队列:实现一个线程安全的队列,支持 Push、Pop、Size 操作
  2. 并发缓存:用 sync.RWMutex 实现一个带过期时间的缓存
  3. 资源池:用 sync.Pool 实现一个数据库连接池
  4. 单例模式:用 sync.Once 实现一个线程安全的配置加载器
  5. 竞态检测:故意写一段有竞态条件的代码,用 -race 检测出来

下一篇预告

下一篇文章,我们将学习 Context 包——Go 并发编程的另一个重要工具。Context 用来控制 goroutine 的取消、超时和传值,是构建大型 Go 应用不可或缺的一部分。

我们下篇见!👋


参考资料:

sync 包的底层实现原理

Mutex 的自旋与休眠策略

Go 的 sync.Mutex 使用了一种结合了自旋锁和休眠锁的混合策略。当一个 goroutine 无法获取锁时,它会先自旋几次(在单核 CPU 上不会自旋),如果仍未获取到锁,则进入休眠状态,等待被唤醒。

package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
    "time"
)

// SpinLock 简化的自旋锁实现
type SpinLock struct {
    state int32
}

func (l *SpinLock) Lock() {
    for !atomic.CompareAndSwapInt32(&l.state, 0, 1) {
        runtime.Gosched() // 让出 CPU
    }
}

func (l *SpinLock) Unlock() {
    atomic.StoreInt32(&l.state, 0)
}

func main() {
    var lock SpinLock
    var wg sync.WaitGroup

    counter := 0
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            lock.Lock()
            counter++
            lock.Unlock()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter)
}

sync.Mutex 在内部使用 sync/atomic 的原子操作和 runtime_Semacquire/runtime_Semrelease 信号量系统调用实现。自旋策略避免了短临界区下的系统调用开销。

WaitGroup 的信号量机制

WaitGroup 通过内部的 64 位计数器(高 32 位为计数器,低 32 位为等待者数量)实现了高效的 goroutine 等待。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    urls := []string{"url1", "url2", "url3"}

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            fmt.Println("Fetching:", u)
        }(url)
    }

    wg.Wait()
    fmt.Println("All done")
}

sync.Once 的双重检查锁定

sync.Once 使用 “compare-and-swap” 操作保证了初始化函数只执行一次。其内部实现使用了原子操作和互斥锁的组合,避免了重复执行。

package main

import (
    "fmt"
    "sync"
)

type Config struct {
    DatabaseURL string
    ServerPort  int
}

var (
    config *Config
    once   sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        config = &Config{
            DatabaseURL: "postgres://localhost:5432/mydb",
            ServerPort:  8080,
        }
        fmt.Println("Config initialized")
    })
    return config
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            cfg := GetConfig()
            fmt.Printf("Port: %d\n", cfg.ServerPort)
        }()
    }
    wg.Wait()
}

sync.Pool 的 GC 策略

sync.Pool 在每次 GC 时会清理所有未引用的对象。这意味着 Get() 获取的对象可能是新创建的,也可能是从池中复用的。这个设计决定了 sync.Pool 适合存储那些创建成本高但生命周期短的对象。

package main

import (
    "bytes"
    "fmt"
    "sync"
)

var pool = sync.Pool{
    New: func() interface{} {
        return bytes.NewBuffer(make([]byte, 0, 4096))
    },
}

func process(data []byte) string {
    buf := pool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        pool.Put(buf)
    }()

    buf.Write(data)
    return buf.String()
}

func main() {
    result := process([]byte("hello world"))
    fmt.Println(result)
}

实战:限流器实现

package main

import (
    "fmt"
    "sync"
    "time"
)

// TokenBucket 令牌桶限流器
type TokenBucket struct {
    capacity int
    tokens   int
    rate     time.Duration
    mutex    sync.Mutex
}

func NewTokenBucket(capacity int, rate time.Duration) *TokenBucket {
    tb := &TokenBucket{
        capacity: capacity,
        tokens:   capacity,
        rate:     rate,
    }
    go tb.refill()
    return tb
}

func (tb *TokenBucket) refill() {
    ticker := time.NewTicker(tb.rate)
    defer ticker.Stop()
    for range ticker.C {
        tb.mutex.Lock()
        if tb.tokens < tb.capacity {
            tb.tokens++
        }
        tb.mutex.Unlock()
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mutex.Lock()
    defer tb.mutex.Unlock()
    if tb.tokens > 0 {
        tb.tokens--
        return true
    }
    return false
}

func main() {
    bucket := NewTokenBucket(5, 200*time.Millisecond)

    for i := 0; i < 15; i++ {
        if bucket.Allow() {
            fmt.Printf("Request %d: allowed\n", i)
        } else {
            fmt.Printf("Request %d: denied\n", i)
        }
        time.Sleep(100 * time.Millisecond)
    }
}

sync 各工具性能对比

工具适用场景性能特点易用性
Mutex保护共享状态自旋+休眠,低开销简单
RWMutex多读少写读锁共享,写锁独占中等
WaitGroup等待 goroutine原子操作,极快简单
Once一次性初始化CAS,只执行一次简单
Pool对象复用减少 GC 压力需注意 Reset
Map并发安全 map分片锁,优于 map+Mutex简单

常见错误与 FAQ

Q: RWMutex 的读者饥饿问题如何避免?

A: Go 1.22 之前的 RWMutex 有一个已知的读者饥饿问题——当有持续不断的读者时,写者可能永远得不到锁。Go 1.22 对此进行了改进,写者会阻止新的读者获取锁。在高写入场景下,如果观察到写操作延迟过高,可以考虑使用 Mutex 替代 RWMutex。

Q: sync.Map 什么时候应该使用?

A: 当满足以下条件时,sync.Map 优于 map + Mutex:

  1. 键值对的写入次数远少于读取次数
  2. 多个 goroutine 读取、写入和覆盖不重叠的键集合
  3. 当多个 goroutine 并发读取同一个键时

Q: sync.Pool 获取的对象需要初始化吗?

A: 必须。sync.Pool 不保证对象的初始状态。使用 pool.Get() 获取的对象可能是之前使用过的,也可能新创建的,必须先重置状态再使用。

Q: WaitGroup 的 Add 应该在启动 goroutine 之前还是之后?

A: 在启动 goroutine 之前。如果在 goroutine 内部调用 Add,而调度的 goroutine 还没开始执行主 goroutine 就调用了 Wait,会导致 race condition。

Q: sync.Once 可以被重置吗?

A: 不能直接重置。一旦 sync.Once 的 Do 方法被执行过,后续所有调用都不会再执行传入的函数。如果需要可重置的 once 行为,需要自己封装结构,用 Mutex 包裹并重新初始化 Once 实例。

Q: sync.Pool 为什么不适合做长期缓存?

A: sync.Pool 中的对象会在每次 GC 时被清理。这是设计原则而非缺陷——Pool 的对象被视为短期、可回收的临时资源。如果把它当长期缓存用,你会发现对象在 GC 后频繁重新创建,缓存命中率低下。长期缓存应使用 map + sync.RWMutex 或第三方缓存库如 bigcache。

Q: sync.Cond 和 channel 的区别是什么?

A: sync.Cond 基于互斥锁和条件变量实现,可以同时唤醒所有等待者(Broadcast),也可以只唤醒一个(Signal)。channel 只能被单个接收者消费。sync.Cond 更灵活但更易出错,大多数时候用 channel 就能满足需求。只有在需要"多个 goroutine 等待同一条件并被全部唤醒"的复杂场景中,才考虑使用 sync.Cond。

Q: 为什么 sync.Mutex 不能被复制?

A: sync.Mutex 内部包含一个状态字段,记录了锁的持有者信息。如果被复制,副本和原始 Mutex 会各自维护独立的状态——一个 goroutine 可能持有副本的锁,而另一个 goroutine 尝试释放原始锁,导致死锁或竞态。Go vet 工具会自动检测包含 sync.Mutex 的结构体是否被值传递。

Q: sync.WaitGroup 的 Wait 可以重复使用吗?

A: 一个 WaitGroup 实例的 Wait 可以在 Add 再次变为正数后重复使用,但必须在 Wait 返回后才能再次 Add,否则行为未定义。更安全的做法是为每组新的并发任务创建一个新的 WaitGroup。

Q: sync.RWMutex 的 RLock 允许嵌套吗?

A: 同一个 goroutine 可以多次调用 RLock(可重入读锁),但对应的 RUnlock 必须调用相同次数才能完全释放。不过嵌套使用会降低代码的可读性,容易引起计数不匹配导致的死锁,建议尽量避免。

最佳实践

1. 锁粒度控制

锁的粒度应该尽可能小,只保护真正需要同步的数据:

// 不好:锁保护整个方法
func (s *Service) Process() {
    s.mu.Lock()
    defer s.mu.Unlock()
    // 不需要锁的操作
    result := expensiveComputation()
    s.data = result
}

// 好:只保护写操作
func (s *Service) Process() {
    result := expensiveComputation()
    s.mu.Lock()
    s.data = result
    s.mu.Unlock()
}

2. 避免死锁

  • 按固定顺序获取多个锁
  • 使用 defer Unlock() 确保锁一定释放
  • 避免在持有锁时调用外部函数

3. 优先使用 channel,必要时用 sync

  • 数据传递和所有权转移:用 channel
  • 共享状态保护:用 sync
  • 一次性的通知广播:用 channel(关闭 channel)

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练