Singleton-单例模式¶
备注
因为单例模式保证了实例的全局唯一性,而且只被初始化一次,所以比较适合全局共享一个实例,且只需要被初始化一次的场景,例如数据库实例、全局配置、全局任务池等。
单例模式又分为饿汉方式和懒汉方式:
饿汉方式: 全局的单例实例在包被加载时创建
懒汉方式: 全局的单例实例在第一次被使用时创建
双重检测
静态内部类
枚举
单例这种设计模式存在哪些问题?为什么会被称为反模式:
1. 单例这种设计模式对于 OOP 的四大特征中的抽象、继承、多态都支持得不好
违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。
一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,
也就相当于损失了可以应对未来需求变化的扩展性。
2. 单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。
如果代码比较复杂,这种调用关系就会非常隐蔽。
3. 单例对代码的扩展性不友好
数据库连接池设计成单例类
当在系统中创建两个数据库连接池:
慢 SQL 独享一个数据库连接池
其他 SQL 独享另外一个数据库连接池
单例类在某些情况下会影响代码的扩展性、灵活性。
所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。
4. 单例对代码的可测试性不友好
单例类这种硬编码式的使用方式,导致无法实现 mock 替换
5. 单例不支持有参数的构造函数
饿汉方式¶
实例:
package singleton
type singleton struct {
}
var ins *singleton = &singleton{}
func GetInsOr() *singleton {
return ins
}
缺点:
因为实例是在包被导入时初始化的,所以如果初始化耗时,会导致程序加载时间比较长
懒汉方式¶
缺点:
非并发安全,在实际使用时需要加锁
不加锁的一个实现:
package singleton
type singleton struct {
}
var ins *singleton
func GetInsOr() *singleton {
if ins == nil {
ins = &singleton{}
}
return ins
}
问题: 在创建 ins 时,如果 ins==nil,就会再创建一个 ins 实例,这时候单例就会有多个实例
带检查锁的一个实现:
import "sync"
type singleton struct {
}
var ins *singleton
var mu sync.Mutex
func GetIns() *singleton {
if ins == nil {
mu.Lock()
if ins == nil {
ins = &singleton{}
}
mu.Unlock()
}
return ins
}
在 Go 开发中,还有一种更优雅的实现方式:
package singleton
import (
"sync"
)
type singleton struct {
}
var ins *singleton
var once sync.Once
func GetInsOr() *singleton {
if ins == nil {
once.Do(func() {
ins = &singleton{}
})
}
return ins
}