单例模式的核心目标是保证一个结构体在程序生命周期内仅被实例化一次,并且提供全局统一的访问入口。在Golang中,由于存在并发执行的goroutine,实现单例模式时需要额外考虑并发安全的问题,否则可能出现多个goroutine同时创建实例的情况。
1. 饿汉式单例实现
饿汉式单例是在程序初始化阶段就完成实例的创建,由于Golang的包初始化机制是线程安全的,因此这种方式天然支持并发安全,不需要额外的同步处理。这种方式的缺点是如果实例初始化耗时较长,会拖慢程序启动速度,适合实例创建成本较低的场景。
package singleton
// 饿汉式单例结构体
type HungrySingleton struct {
Config string
}
// 程序启动时直接初始化实例
var hungryInstance = &HungrySingleton{
Config: "default_config",
}
// 获取实例的全局方法
func GetHungryInstance() *HungrySingleton {
return hungryInstance
}
2. 懒汉式单例实现(非线程安全)
懒汉式单例是在第一次调用获取实例的方法时才创建实例,相比饿汉式可以延迟初始化时间,减少程序启动开销。但下面这种简单实现方式没有做并发控制,多线程场景下会创建多个实例,实际生产环境不建议使用。
package singleton
type LazySingleton struct {
Data string
}
var lazyInstance *LazySingleton
// 非线程安全的获取实例方法
func GetLazyInstance() *LazySingleton {
if lazyInstance == nil {
lazyInstance = &LazySingleton{
Data: "lazy_data",
}
}
return lazyInstance
}
3. 加锁的懒汉式单例实现
为了修复非线程安全的问题,可以给实例创建过程加互斥锁,保证同一时间只有一个goroutine能进入实例创建的逻辑。这种方式的缺点是每次获取实例都需要加锁,会带来额外的性能开销,高并发场景下性能表现不佳。
package singleton
import "sync"
type LockSingleton struct {
Version string
}
var (
lockInstance *LockSingleton
lock sync.Mutex
)
// 加锁的获取实例方法
func GetLockInstance() *LockSingleton {
lock.Lock()
defer lock.Unlock()
if lockInstance == nil {
lockInstance = &LockSingleton{
Version: "v1.0",
}
}
return lockInstance
}
4. 双重检查锁的懒汉式单例实现
双重检查锁是在加锁的基础上,先判断实例是否为空再获取锁,避免每次调用都加锁的性能损耗。不过需要注意,Golang中普通变量的读写无法保证原子性,这种实现方式在部分场景下仍然可能出现问题,不是最优解。
package singleton
import "sync"
type DoubleCheckSingleton struct {
Name string
}
var (
dcInstance *DoubleCheckSingleton
dcLock sync.Mutex
)
// 双重检查锁的获取实例方法
func GetDoubleCheckInstance() *DoubleCheckSingleton {
if dcInstance == nil {
dcLock.Lock()
defer dcLock.Unlock()
if dcInstance == nil {
dcInstance = &DoubleCheckSingleton{
Name: "double_check",
}
}
}
return dcInstance
}
5. 基于sync.Once的单例实现(推荐)
sync.Once是Golang标准库提供的工具,它可以保证传入的函数在程序运行期间仅执行一次,并且是线程安全的,非常适合用来实现单例模式。这种方式既实现了延迟初始化,又保证了并发安全,还没有额外的性能损耗,是Golang中实现单例模式的最优方案。
package singleton
import "sync"
type OnceSingleton struct {
ID int
}
var (
onceInstance *OnceSingleton
once sync.Once
)
// 基于sync.Once的获取实例方法
func GetOnceInstance() *OnceSingleton {
once.Do(func() {
onceInstance = &OnceSingleton{
ID: 1001,
}
})
return onceInstance
}
6. 不同实现方式对比
以下是几种常见单例实现方式的特性对比,开发者可以根据实际需求选择:
| 实现方式 | 并发安全 | 延迟初始化 | 性能表现 | 适用场景 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 优 | 实例创建成本低的场景 |
| 非线程安全懒汉式 | 否 | 是 | 优 | 单线程场景 |
| 加锁懒汉式 | 是 | 是 | 一般 | 并发量较低的场景 |
| 双重检查锁懒汉式 | 理论上存在风险 | 是 | 较好 | 不推荐使用 |
| sync.Once懒汉式 | 是 | 是 | 优 | 绝大多数生产场景 |
7. 单例模式使用注意事项
- 单例实例如果是全局共享的,需要注意其内部状态是否被多个goroutine同时修改,必要的时候需要给单例内部的操作加锁保证线程安全。
- 单例模式会增加代码的耦合度,不要过度使用,仅当确实需要全局唯一实例的时候才考虑使用。
- 如果单例需要支持不同的配置参数,不建议直接修改单例的实现,可以结合工厂模式或者配置注入的方式实现。