sync.RWMutex(读写互斥锁)
sync.RWMutex
是 Go 语言标准库 sync
包提供的一种读写锁。它是一种特殊的互斥锁,允许多个读操作并行进行,但写操作是完全独占的。
这意味着:
- 可以多个 Goroutine 同时持有「读锁」。
- 同时只能有一个 Goroutine 持有「写锁」。
- 写锁和读锁是互斥的。也就是说,当一个 Goroutine 持有写锁时,其他所有 Goroutine 都无法获得读锁或写锁。同样,当一个或多个 Goroutine 持有读锁时,任何 Goroutine 都无法获得写锁。
它内部维护了两个状态和两个队列:
- 读者计数:记录当前有多少个读者正在访问资源。
- 写者标记:标记是否有写者正在等待或访问资源。
- 读者队列:当有写者等待时,后续的读者会进入队列等待,以防止写者被“饿死”(一直得不到锁)。
- 写者队列:等待获取写锁的 Goroutine 队列。
二、核心方法
sync.RWMutex
提供了五个关键方法:
func (rw *RWMutex) Lock()
- 获取写锁- 如果锁已被其他 Goroutine 持有(无论是读锁还是写锁),调用
Lock()
的 Goroutine 都会阻塞,直到锁可用。 - 一旦获取成功,它将阻止所有其他读者和写者获取锁。
- 如果锁已被其他 Goroutine 持有(无论是读锁还是写锁),调用
func (rw *RWMutex) Unlock()
- 释放写锁- 释放由
Lock()
获取的写锁。
- 释放由
func (rw *RWMutex) RLock()
- 获取读锁- 如果当前没有写者持有写锁,也没有写者在等待,那么读锁会立即获取成功,读者计数加一。
- 如果当前有写者持有锁或正在等待,那么调用
RLock()
的 Goroutine 会阻塞,直到获取到读锁。
func (rw *RWMutex) RUnlock()
- 释放读锁- 释放由
RLock()
获取的读锁,将读者计数减一。 - 当最后一个读者释放锁后,会唤醒等待的写者(如果存在)。
- 释放由
func (rw *RWMutex) RLocker() sync.Locker
- 返回一个实现了Locker
接口的读锁- 这个返回的锁的
Lock()
和Unlock()
方法实际上调用的是RWMutex
的RLock()
和RUnlock()
。这主要用于需要传递sync.Locker
接口的地方,但其内部实现是读锁。
- 这个返回的锁的
适合场景
sync.RWMutex
的唯一最佳适用场景是:读操作频率远远高于写操作频率,并且临界区的代码执行耗时较长。
为什么?
- 优势:在高读低频写的场景下,读写锁允许读操作并行,这极大地减少了 Goroutine 的等待时间,从而显著提升程序性能。如果使用普通的
sync.Mutex
,所有读操作也会串行化,成为性能瓶颈。 - 劣势:
sync.RWMutex
的内部实现比sync.Mutex
更复杂,其维护读者计数、处理队列等操作本身就有额外的开销。如果临界区代码非常简单(例如,只是一个赋值操作),那么使用RWMutex
带来的性能提升可能还抵不上其自身的开销。
简单总结:
读多写少
+临界区耗时
-> 使用sync.RWMutex
,性能提升明显。写多读少
或临界区简单
-> 使用sync.Mutex
可能更简单、更高效。
详细场景举例说明
全局配置信息的热更新
这是一个非常典型的用例。服务器的配置通常在启动时加载,运行期间可能需要通过管理命令或 API 进行热更新。读取配置的操作(例如,处理每一个 HTTP 请求)非常频繁,而更新配置的操作非常稀少。
不使用锁(错误示范):var config map[string]string // 全局配置
func handleRequest() {
// 在读取 config 的过程中,如果另一个 Goroutine 更新了 config
// 可能会导致部分旧配置和新配置混用,或者程序崩溃
useConfig := config
// ... 处理请求,使用 useConfig
}
func updateConfig(newConfig map[string]string) {
// 直接赋值,在并发情况下极其危险
config = newConfig
}
使用 sync.RWMutex
:package main
import (
"fmt"
"sync"
"time"
)
var (
config map[string]string
configMu sync.RWMutex // 专门用于保护 config 的读写锁
)
// LoadConfig 模拟从文件或数据库加载配置
func LoadConfig() map[string]string {
return map[string]string{
"host": "127.0.0.1",
"port": "8080",
}
}
// InitConfig 初始化配置
func InitConfig() {
config = LoadConfig()
}
// GetConfig 获取配置(读操作)
func GetConfig(key string) (string, bool) {
configMu.RLock() // 获取读锁
defer configMu.RUnlock() // 函数返回前释放读锁
// 多个 Goroutine 可以同时执行到这里
value, ok := config[key]
return value, ok
}
// ReloadConfig 重载配置(写操作)
func ReloadConfig() {
newConfig := LoadConfig()
configMu.Lock() // 获取写锁
defer configMu.Unlock() // 函数返回前释放写锁
// 获取写锁后,会阻塞直到所有现有的读锁释放,并且阻止新的读锁获取
config = newConfig
fmt.Println("Config reloaded at", time.Now())
}
// 模拟大量的读取操作
func main() {
InitConfig()
// 模拟一个后台定时更新配置的 Goroutine
go func() {
for range time.Tick(5 * time.Second) { // 每5秒更新一次(写操作很少)
ReloadConfig()
}
}()
// 模拟大量的客户端请求,频繁读取配置
for i := 0; i < 100; i++ {
go func(id int) {
for {
host, _ := GetConfig("host") // 高频率的读操作
port, _ := GetConfig("port")
// fmt.Printf("Goroutine %d: Connecting to %s:%s\n", id, host, port)
_ = host + port // 假装使用一下
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 让主 Goroutine 不要退出
time.Sleep(1 * time.Minute)
}
在这个例子中:
- 读操作 (
GetConfig
):被 100 个 Goroutine 高频调用,使用RLock()
允许它们并发执行,效率极高。 - 写操作 (
ReloadConfig
):每 5 秒才执行一次,使用Lock()
来保证更新配置时是绝对安全的,不会有读操作读到一半更新了一半的中间状态。
如果这里使用普通的 sync.Mutex
,那 100 个 Goroutine 的每次读取都会相互阻塞,性能会差很多。
内存缓存
另一个经典场景是实现一个内存中的键值对缓存。
package main |
在这个例子中:
Get
操作(读):被应用程序频繁调用,使用RLock()
允许高并发。Set
和Cleanup
操作(写):相对不那么频繁,使用Lock()
来保证数据的一致性。
重要注意事项
- 不可递归获取写锁:一个已经持有写锁的 Goroutine 再次调用
Lock()
会导致死锁。同样,持有读锁时调用Lock()
也会死锁(因为写锁需要等待所有读锁释放,包括自己这个)。 - 写锁优先:为了防止写者被“饿死”(即一直有读者来导致写者永远无法获取锁),当有一个写者在等待时,
RWMutex
会阻止后续新的读者获取读锁。后续的读者会等待写者完成后再进行。 RUnlock
未加锁的 Panic:释放一个未持有的读锁会引发 panic,写锁同理。- 锁的拷贝:和
sync.Mutex
一样,sync.RWMutex
在第一次使用后就不应该被拷贝。应该始终通过指针传递。
sync.Mutex
(互斥锁)
sync.Mutex
是 Go 语言标准库 sync
包提供的一种互斥锁。它是 “Mutual Exclusion”(互斥)的缩写。它的核心思想非常简单:保证在任何时候,最多只有一个 Goroutine 能进入临界区。
可以把它想象成一个房间的钥匙,这个房间一次只允许一个人进入。一个人拿到钥匙进入房间后,会把门锁上。其他人要想进入,必须在门口等待,直到里面的人出来并把钥匙交给下一个等待者。
核心方法
sync.Mutex
只有两个方法:
func (m *Mutex) Lock()
- 用于获取互斥锁。
- 如果锁已被其他 Goroutine 持有,则调用
Lock()
的 Goroutine 会阻塞,直到锁被释放并且自己成功获取到锁。 - 如果锁当前无人持有,则当前 Goroutine 会立即获取锁。
func (m *Mutex) Unlock()
- 用于释放互斥锁。
- 重要:必须在持有锁的 Goroutine 中调用
Unlock()
,否则会引发运行时 panic(例如,尝试释放一个未锁定的锁)。
零值可用
sync.Mutex
的零值就是一个未锁定的互斥锁,这意味着可以直接声明并使用它,无需初始化。
var mu sync.Mutex // 声明即可使用,mu 此时是未锁定的状态 |
使用模式与示例
sync.Mutex
的核心作用是保护共享资源,确保对它们的访问是串行的。
场景:银行账户转账(保护多个相关变量)
package main |
关键点:
- 无论是写操作(
Deposit
,Withdraw
)还是读操作(Balance
),都需要使用Lock()
进行保护。 - 使用
defer mu.Unlock()
是一种非常安全且常见的做法,它能确保锁即使在函数发生 panic 的情况下也能被释放,避免死锁。 - 这个例子中操作非常快(
+=
,-=
,>=
),所以使用Mutex
很合适。即使有 200 个 Goroutine,它们也会快速地串行执行完毕。
sync.Mutex
与 sync.RWMutex
的对比
特性 | sync.Mutex (互斥锁) |
sync.RWMutex (读写锁) |
---|---|---|
核心思想 | 完全互斥,独占访问 | 读写分离,共享读,独占写 |
锁类型 | 一种锁:Lock() / Unlock() |
两种锁:写锁 (Lock() /Unlock() ) 和 读锁 (RLock() /RUnlock() ) |
读操作并发性 | 否。所有操作(包括读)都必须串行。 | 是。多个 Goroutine 可以同时持有读锁。 |
写操作并发性 | 否。写操作自然也是串行的。 | 否。写操作是独占的,和任何其他操作(读或写)都互斥。 |
性能开销 | 低。实现简单,逻辑判断少。 | 高。需要维护读者计数、处理写者等待队列等,内部逻辑更复杂。 |
适用场景 | 通用场景。特别是: 1. 读写操作频率相当。 2. 临界区代码执行非常快(例如简单的赋值、加减)。 3. 写操作很多。 4. 不需要区分读和写的逻辑。 |
特定场景:读多写少。 1. 读操作频率远高于写操作(例如 90% 是读,10% 是写)。 2. 临界区代码执行耗时较长(例如读取配置、复杂查询),使得并发读带来的收益能覆盖 RWMutex 自身的开销。 |
零值可用 | 是 | 是 |
如何选择
可以根据以下逻辑来决定使用哪种锁:
graph TD |
简单总结:
- 不确定该用哪个时,优先使用
sync.Mutex
。它更简单,不易用错,并且在很多情况下性能足够好。 - 只有在确凿的证据(例如性能分析表明读操作成为了瓶颈)表明程序是 “读多写少” 且 临界区较耗时 时,才考虑将
sync.Mutex
替换为sync.RWMutex
。
一个直观的性能对比示例
下面的例子模拟了不同锁在高并发读操作下的性能差异。
package main |
运行结果(示例,具体数字因机器而异):Mutex read: 12.456ms
RWMutex read: 1.789ms
RWMutex was 6.96x faster for reads
这个结果清晰地展示了在纯读操作的高并发场景下,RWMutex
的巨大性能优势。但如果在这个测试中加入哪怕很少的写操作,RWMutex
的优势就会缩小,因为写锁会强制后续的读操作等待。
sync.Once
sync.Once
是 Go 语言标准库 sync
包提供的一个结构体,它用于保证某个操作在整个程序运行期间只被执行一次,而且是并发安全的。
它的核心思想是“懒加载”(Lazy Initialization):延迟昂贵的初始化操作,直到真正需要它的时候才执行,并且确保即使有多个 Goroutine 同时需要,它也只会初始化一次。
核心方法与工作原理
核心方法
sync.Once
只有一个方法:
func (o *Once) Do(f func())
- 参数
f
是一个无参数、无返回值的函数,包含了希望只执行一次的代码。 - 无论调用
once.Do(f)
多少次,无论在多少个 Goroutine 中调用,函数f
都只会被执行一次。 - 所有调用
once.Do(f)
的 Goroutine 在f
执行结束后才会继续执行,从而保证它们能获取到初始化后的结果。
- 参数
工作原理(内部机制)
sync.Once
内部通过一个互斥锁 (sync.Mutex
) 和一个原子布尔标志(或状态位)来实现:
- 标志位 (
done
):通常是一个uint32
,使用原子操作(atomic.CompareAndSwapUint32
等)来记录函数f
是否已被执行。原子操作保证了在并发环境下读写这个标志的绝对安全。 - 互斥锁 (
m
):用于在标志位检查后、函数执行前,创建一个临时的临界区,防止多个 Goroutine 同时执行f
。
其工作流程可以简化为:
- 检查
done
标志。如果已设置为“已完成”,则立即返回。 - 如果未完成,则获取互斥锁。
- 再次检查
done
标志(双检查锁定模式)。这是为了避免在获取锁的期间,已经有其他 Goroutine 执行完了f
并释放了锁。 - 如果第二次检查仍未完成,则执行函数
f
。 - 执行完毕后,通过原子操作将
done
标志置为“已完成”,然后释放互斥锁。
适合场景
sync.Once
的适用场景非常明确,即 “延迟初始化” 和 “单例模式”。只要需求是“确保某段代码只跑一次”,它就是最佳选择。
- 初始化配置:从文件、环境变量或远程配置中心加载配置信息。
- 建立数据库连接池:在程序启动时建立连接池,而不是在每次处理请求时都建立新连接。
- 创建单例对象:确保某个类的实例全局只有一个。
- 加载本地缓存数据:在第一次访问时从数据库加载数据到内存缓存。
- 执行一次性的设置代码:例如,在测试中注册一个驱动。
场景一:单例模式(数据库连接池)
这是最经典的使用场景。在 Web 服务中,通常只需要一个全局的数据库连接池,所有请求共享它。
错误做法(非并发安全):var dbConn *sql.DB
func getDB() *sql.DB {
if dbConn == nil {
dbConn, _ = sql.Open("mysql", "user:password@/dbname") // 非线程安全!
}
return dbConn
}
// 多个 Goroutine 同时调用 getDB() 可能会创建多个连接池,导致资源泄露。
使用 sync.Once
的正确做法:package main
import (
"database/sql"
"fmt"
"sync"
_ "github.com/go-sql-driver/mysql"
)
var (
db *sql.DB
dbOnce sync.Once // 专门用于初始化 db 的 Once
)
func getDB() *sql.DB {
// 初始化操作被封装在 Do 的回调函数里
dbOnce.Do(func() {
fmt.Println("Initializing database connection...")
var err error
// 注意:这里使用了 :=,所以 db 是局部变量,必须赋值给全局的 db
db, err = sql.Open("mysql", "user:password@tcp(localhost:3306)/testdb")
if err != nil {
panic(err)
}
db.SetMaxOpenConns(10)
})
return db
}
func main() {
var wg sync.WaitGroup
// 模拟 10 个并发请求,每个请求都需要获取数据库连接
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d is getting DB instance...\n", id)
conn := getDB()
// 使用 conn 执行查询等操作...
_ = conn // 假装使用一下
fmt.Printf("Goroutine %d got it!\n", id)
}(i)
}
wg.Wait()
// 最终输出只会有一句 "Initializing database connection..."
}
输出结果:Goroutine 9 is getting DB instance...
Goroutine 7 is getting DB instance...
Goroutine 0 is getting DB instance...
Initializing database connection... # <-- 只出现一次!
Goroutine 0 got it!
Goroutine 7 got it!
...
Goroutine 9 got it!
场景二:加载应用配置
配置通常在程序启动时加载一次,之后所有地方都读取这份配置。
package main |
重要注意事项
done
状态与Once
实例绑定:每个sync.Once
实例都独立跟踪自己负责的那个操作是否已完成。如果有多个不同的初始化操作,应该为每个操作创建单独的sync.Once
实例。var (
onceA sync.Once
onceB sync.Once
)
// onceA.Do(initA) 和 onceB.Do(initB) 互不影响函数
f
执行失败后的行为:如果once.Do(f)
中的函数f
执行时发生了 panic 或错误退出,sync.Once
会认为该操作已经完成(因为done
标志被置位)。后续再调用once.Do(...)
将不会再次尝试执行f
。需要自行在f
内部处理错误。var once sync.Once
once.Do(func() {
fmt.Println("This will run only once, even if it panics.")
panic("oops!")
})
// 第二次调用,函数不会被执行
once.Do(func() {
fmt.Println("This will NEVER be printed.")
})不要试图在
f
中递归调用once.Do
:这会导致死锁。因为内部的Do
调用在等待外部的Do
完成(释放锁),而外部的Do
又被内部的Do
阻塞。// 错误示例:会导致死锁
once.Do(func() {
once.Do(func() { // 死锁在这里发生
fmt.Println("Hello")
})
})零值可用:
sync.Once
的零值就是一个有效的、未执行过操作的实例,可以直接使用。
sync.Once
和init函数对比
sync.Once
是一个简单、强大且高效的同步工具,它解决了并发环境下“一次性初始化”的通用问题。其设计巧妙地结合了原子操作和互斥锁,既保证了性能又确保了正确性。当需要实现单例、加载配置或进行任何只需执行一次的设置时,sync.Once
应该是的首选方案。
sync.Once
和 init
函数都是 Go 语言中用于实现“一次性初始化”的机制,但它们在设计理念、执行时机和控制方式上有着根本的区别。
核心概念
init
函数- 它是一个特殊的函数,没有任何参数和返回值。
- 每个包可以拥有多个
init
函数(甚至一个源文件也可以有多个)。 - 它由 Go 运行时在程序启动时自动调用,用于初始化包级别的变量或执行包所需的准备工作。
- 调用顺序:从最底层的导入包开始,逐步向上到
main
包,在main
函数的执行之前完成所有init
。
sync.Once
- 它是
sync
包中的一个结构体类型,通过其Do
方法来实现一次性操作。 - 它 延迟 了初始化操作,直到第一次真正需要它的时候才执行(懒加载)。
- 它保证了在并发环境下,初始化操作也只会被执行一次。
- 它是
详细对比
特性 | init 函数 |
sync.Once |
---|---|---|
执行时机 | 启动时(Early)。在 main 函数之前,由运行时自动、隐式调用。 |
运行时(On-Demand)。在代码中首次调用 once.Do() 时执行,是显式的。 |
调用方式 | 自动隐式。开发者无法控制其调用时机和顺序(除了依赖导入顺序)。 | 手动显式。开发者完全控制何时、在何处进行初始化。 |
并发安全性 | 天然安全。init 函数在程序启动的单线程环境中运行,不存在并发问题。 |
设计用于并发。其核心目的就是在高并发场景下安全地执行一次性初始化。 |
执行失败的影响 | 如果 init 函数 panic ,会导致整个程序启动失败。 |
如果 Do 中的函数 panic ,Once 会认为其已执行完成,后续调用不会再执行,但不会影响程序其他部分。 |
错误处理 | 非常困难。init 中很难向外部返回错误,通常只能 panic 或记录日志。 |
相对灵活。可以在 Do 的函数内部进行错误处理,并将错误保存到全局变量中供后续检查。 |
依赖关系 | 依赖通过包的导入顺序来隐式管理。复杂的依赖关系可能难以理解和维护。 | 依赖关系在代码逻辑中显式体现。初始化发生在真正需要该依赖时,逻辑更清晰。 |
主要用途 | 初始化包级别的必需且轻量的全局状态、注册驱动、验证环境等。 | 延迟初始化昂贵的资源(数据库连接、缓存加载)、实现单例模式。 |
对启动性能的影响 | 可能较大。所有 init 都会在启动时执行,如果初始化操作很耗时,会明显拖慢程序启动速度。 |
几乎无影响。将耗时操作延迟到运行时,加快启动速度,实现“按需加载”。 |
适合场景与代码举例
场景一:注册数据库驱动(适合使用 init
)
数据库驱动需要在程序开始使用前就注册到 Go 的 sql
包中,这是一个典型的启动时必须完成的轻量级操作。
// 假设在第三方驱动包中:github.com/example/mydriver |
// 在主程序中 |
为什么用 init
? 因为驱动注册是应用程序运行的前提,必须在任何数据库操作之前完成,启动时自动执行是最合理的。
场景二:按需加载大型缓存(适合使用 sync.Once
)
一个大型的产品目录缓存,加载需要从数据库读取大量数据,非常耗时。只有在处理第一个请求时才加载它,而不是在程序启动时就加载。
package main |
输出:Loading massive product cache from DB... This takes time. # <-- 只出现一次
Request 4 got product: Laptop
Request 0 got product: Laptop
...
Request 3 got product: Laptop
为什么用 sync.Once
? 因为缓存加载是昂贵且非启动必需的操作。延迟加载显著加快了程序启动速度,并且节约了资源(如果某些实例永远没收到查询请求,就永远不用加载缓存)。
如何选择
问自己:这个初始化是程序运行的绝对前提吗?
- 是 -> 优先考虑
init
。 (例如:注册驱动、解析必须的配置、验证环境变量) - 否 -> 考虑
sync.Once
。
- 是 -> 优先考虑
问自己:这个初始化操作昂贵吗(耗时/耗资源)?
- 是 -> 绝对应该使用
sync.Once
进行延迟加载。 (例如:建立连接池、加载大文件到内存、预计算大量数据) - 否 -> 两者都可以,但
init
更简单。
- 是 -> 绝对应该使用
问自己:需要处理初始化过程中的错误吗?
- 需要 ->
sync.Once
更合适,可以在其函数内将错误赋值给一个全局变量,供调用者检查。 - 不需要(错误应导致程序失败)->
init
中直接panic
也是一种选择。
- 需要 ->
问自己:这个初始化是否依赖于程序运行时的状态?
- 是 -> 必须使用
sync.Once
,因为init
的执行远早于main
,无法获取运行时状态。 - 否 -> 两者都可以。
- 是 -> 必须使用
简单总结:
init
用于启动时、简单、必需的设置。sync.Once
用于运行时、昂贵、按需的初始化。
在实践中,sync.Once
的使用频率往往更高,因为它提供了更好的控制力和性能特性,更符合现代应用程序的设计需求。而 init
函数则更多地被封装在第三方库内部,用于完成其内部的准备工作。
sync
包其他函数
当然有。sync
包是 Go 语言并发编程的基石,除了 Mutex
, RWMutex
, 和 Once
之外,还提供了多个非常实用的同步原语。
sync.WaitGroup
(等待组)
说明
WaitGroup
用于等待一组 Goroutine 执行完成。主 Goroutine 通过 Add
方法设置需要等待的 Goroutine 数量,每个工作 Goroutine 在结束时调用 Done
方法。主 Goroutine 可以调用 Wait
方法阻塞,直到所有工作 Goroutine 都完成并调用了 Done
。
核心方法
func (wg *WaitGroup) Add(delta int)
: 增加或减少需要等待的 Goroutine 数量。通常在启动 Goroutine 前调用。func (wg *WaitGroup) Done()
: 表示一个 Goroutine 已完成,等价于Add(-1)
。必须在 Goroutine 结束时调用,通常使用defer wg.Done()
。func (wg *WaitGroup) Wait()
: 阻塞当前 Goroutine,直到所有 Goroutine 都调用了Done
。
适用场景
需要阻塞主线程,等待所有并发任务执行完毕后再继续。 这是 Go 并发中最常用的同步工具之一。
示例:批量处理任务
package main |
sync.Cond
(条件变量)
说明
Cond
用于在多个 Goroutine 之间进行事件通知。它让一组 Goroutine 在满足某种条件时被唤醒执行,而不是通过循环检查(忙等待)的方式,从而节省 CPU 资源。它总是与一个 Locker
(通常是 Mutex
)关联使用。
核心方法
func NewCond(l Locker) *Cond
: 创建一个条件变量,需要传入一个锁(通常是&sync.Mutex{}
)。func (c *Cond) Wait()
: 1) 解锁关联的互斥锁;2) 阻塞当前 Goroutine,等待通知;3) 被唤醒后,重新锁定互斥锁。func (c *Cond) Signal()
: 唤醒一个正在Wait
的 Goroutine。func (c *Cond) Broadcast()
: 唤醒所有正在Wait
的 Goroutine。
适用场景
多个 Goroutine 需要等待某个特定条件或共享状态变为真。 例如,生产者-消费者模型、等待资源就绪、并发任务的协调。
示例:生产者-消费者模型
package main |
sync.Pool
(临时对象池)
说明
Pool
用于存储和复用临时对象,以减少内存分配和垃圾回收(GC)的压力。它可以安全地被多个 Goroutine 同时使用。从 Pool 中获取对象时,会尝试返回池中的现有对象;如果池为空,则调用 New
函数创建一个新对象。
适用场景
频繁创建和销毁大量相同类型的临时对象,且这些对象的创建成本较高。 例如:
- 编码解码时的缓冲区(如
bytes.Buffer
)。 - 网络连接池的中间层。
- 避免大型结构体的重复分配。
注意:Pool 中的对象可能会在任何时候被 GC 无条件清除,因此不能用它来实现诸如“数据库连接池”这类需要长期存活对象的池(应用级连接池通常用其他方式实现)。
示例:复用 bytes.Buffer
package main |
sync.Map
(并发映射)
说明
sync.Map
是一个并发安全的映射(map)结构。与使用 sync.Mutex
或 sync.RWMutex
保护普通的 map
不同,sync.Map
被优化用于以下特定场景:
- 键值对只写一次,但会被多次读取(例如,在启动时初始化的配置映射)。
- 多个 Goroutine 读写不相交的键(每个 Goroutine 操作不同的键)。
在这些场景下,sync.Map
比使用互斥锁的传统方式性能更好。但在常规的“读多写少”但写操作涉及大量不同键的场景中,使用 RWMutex
保护的普通 map
可能更合适。
核心方法
Store(key, value interface{})
: 存储键值对。Load(key interface{}) (value interface{}, ok bool)
: 读取键对应的值。LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
: 读取键值,如果不存在则存储。Delete(key interface{})
: 删除键值对。Range(func(key, value interface{}) bool)
: 遍历所有键值对。
适用场景
非常适合缓存、元数据存储、只加载一次的数据字典等。
示例:用作全局缓存
package main |
总结
原语 | 核心用途 | 适用场景 |
---|---|---|
WaitGroup |
等待一组 Goroutine 完成 | 批量任务并行处理,等待所有结果返回。 |
Cond |
Goroutine 间的事件通知 | 生产者-消费者、等待资源就绪、避免忙等待。 |
Pool |
复用临时对象,降低 GC 压力 | 高频创建/销毁昂贵对象(如缓冲区)。 |
Map |
并发安全的映射 | 缓存、只写一次的映射、操作不相交的键。 |