概述
在 Go 中,线程安全(并发安全)的数据结构主要用于在多个 goroutine 之间安全地共享和操作数据。Go 倡导通过 goroutine + channel 进行并发控制,但在一些场景下,需要锁或原子操作来保证数据结构的安全性。
1. 标准库中的线程安全机制
Go 标准库提供了实现线程安全所需的基础工具:
(1) sync 包提供了基本的同步原语,用于实现线程安全的数据访问:
sync.Mutex/sync.RWMutex:互斥锁与读写锁,用于保护共享资源。sync.Map:线程安全 map,适合读多写少的场景。sync.Once:保证某个操作只执行一次,常用于单例模式。sync.WaitGroup:等待一组 goroutine 完成,常用于任务协调。
(2) sync/atomic 包提供了底层的原子操作,用于对基本数据类型(如 int32、int64、uint32 等)进行线程安全的操作,避免锁的开销。
- 提供底层原子操作,如
AddInt32、LoadInt32、StoreInt32等。 - 适合计数器或标志位,不适合复杂逻辑。
var counter int32
func increment() {
atomic.AddInt32(&counter, 1)
}
func main() {
for i := 0; i < 100; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println(atomic.LoadInt32(&counter)) // 输出: 100
}
(3) Channel是并发安全的通信机制,可用于在 goroutine 之间安全地传递数据。
虽然 channel 本身不是传统的数据结构,但它可以用来实现线程安全的队列。
示例(线程安全的队列):
gofunc main() {
ch := make(chan int, 100)
go func() {
ch <- 42 // 生产者
}()
value := <-ch // 消费者
fmt.Println(value) // 输出: 42
}
- Go 原生的并发安全通信机制,常用于实现生产者-消费者模型。
- 更像“数据流通道”,不适合通用队列的需求。
2. 常见的线程安全数据结构实现
标准库没有直接提供线程安全的切片、队列、栈数据结构。但是可以通过锁封装实现线程安全:
- 线程安全切片:用
Mutex封装append和索引访问。 - 线程安全队列:用切片+锁实现
Enqueue和Dequeue。 - 线程安全栈:用切片+锁实现
Push和Pop。
(1) 线程安全的切片
通过 sync.Mutex 保护切片的操作:
gotype SafeSlice struct {
mu sync.Mutex
items []int
}
func (s *SafeSlice) Append(item int) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *SafeSlice) Get(index int) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if index < 0 || index >= len(s.items) {
return 0, errors.New("index out of range")
}
return s.items[index], nil
}
(2) 线程安全的队列
可以使用 sync.Mutex 封装切片实现队列,或者直接使用 channel:
gotype SafeQueue struct {
mu sync.Mutex
items []int
}
func (q *SafeQueue) Enqueue(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
}
func (q *SafeQueue) Dequeue() (int, error) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.items) == 0 {
return 0, errors.New("queue is empty")
}
item := q.items[0]
q.items = q.items[1:]
return item, nil
}
(3) 线程安全的栈
类似地,通过 sync.Mutex 封装切片实现栈:
gotype SafeStack struct {
mu sync.Mutex
items []int
}
func (s *SafeStack) Push(item int) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *SafeStack) Pop() (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
return 0, errors.New("stack is empty")
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, nil
}
3. 工具对比表格
| 工具/机制 | 类型/功能 | 适用场景 | 优点 | 局限性 |
|---|---|---|---|---|
sync.Mutex | 互斥锁 | 任意共享资源保护 | 简单直接,适用面广 | 只能单线程进入,读多写少效率不高 |
sync.RWMutex | 读写锁 | 读多写少的场景 | 支持多读并发,性能优于纯 Mutex | 写操作仍然互斥,加锁顺序需谨慎 |
sync.Map | 并发安全 map | 高并发读、低并发写(缓存、配置表) | 内部优化,读性能高 | 写多时性能差,不支持完整 map 语法 |
sync.Once | 单次执行 | 单例模式、初始化共享资源 | 保证执行一次,线程安全 | 仅适合“一次性”的场景 |
sync.WaitGroup | 任务同步 | 等待一组 goroutine 完成 | 使用简单,常用于并发任务管理 | 不保护数据,仅用于同步 |
sync/atomic | 原子操作 | 计数器、标志位 | 无锁高性能,底层原子性保证 | 仅适合单变量,复合操作需额外保护 |
| Channel | 并发通信机制 | 生产者-消费者、任务分发 | 天然并发安全,简化同步 | 不适合随机访问或复杂数据结构 |
4. 使用注意事项
- 能用 channel 就用 channel:Go 的推荐方式是“通过通信共享内存”。
- 读多写少 →
sync.RWMutex或sync.Map。 - 简单计数器/标志位 →
sync/atomic。 - 写操作频繁 → 普通
map+Mutex更优。 - 锁的使用要注意顺序,避免死锁。
5. 总结
Go 没有直接内置大量线程安全的数据结构,但通过以下方式可以实现线程安全(并发安全):
- 标准库工具:
sync.Mutex、sync.RWMutex、sync.Map、sync.Once、sync.WaitGroup、sync/atomic、channel。 - 自定义封装:用锁包装切片、队列、栈等结构。
核心原则是:
不要通过共享内存来通信,而是通过通信来共享内存。
即优先使用 channel + goroutine 模型,只有在必要时再使用锁或原子操作,保持代码简洁与高效。
Go线程安全数据结构详解
1580

被折叠的 条评论
为什么被折叠?



