单例模式是创建型模式之一,保证每个类只有一个实例,因此节省内存,加快对象访问速度,对象访问被公用的情况适合使用,如:
- 配置文件的初始化,一个进程中的所有线程公用一份配置数据。
- 链接池类的初始化,一般小的项目中只有一个链接池
- 日志,id生成器等全局唯一的类。
实现方式:
饥饿加载的方式
使用 init 函数初始化为包变量。
package Single
import (
"fmt"
)
var SingleInstance *single
type single struct{}
func init() {
SingleInstance = new(single)
}
懒加载的方式
- 通过加锁的方式保证并发时只有一个线程写
package Single
import (
"fmt"
)
var lock = &sync.Mutex{}
var singleInstance *single
type single struct{}
func GetInstance() *single {
if singleInstance == nil {
lock.Lock()
defer lock.Unlock()
if singleInstance == nil {
singleInstance = new(single)
}
}
return singleInstance
}
- 使用标准库提供的
sync.once实现,并发场景下是线程安全的。
package Single
import (
"fmt"
)
var (
singleInstance *single
once sync.Once
)
type single struct{}
func GetInstance() *single {
if singleInstance == nil {
once.Do(func() {
singleInstance = new(single)
})
}
return singleInstance
}
以上三种方式均可实现单例模式,饿汉式初始化的方式不支持延迟加载,有些初始化耗时较长的操作都可以使用该种方式实现,等到系统繁忙使用到该资源时,能够有效提高系统性能,这样能避免在程序运行时再去初始化,导致性能问题。相反懒汉式支持懒加载,但不符合有问题及早暴露的原则,需要分场景使用。
测试代码
func main() {
for i := 0; i < 10; i++ {
// 地址相同
// instance := GetInstance()
// fmt.Printf("%p\n", instance)
// 只初始化一次
go GetInstance()
}
fmt.Scanln()
}
存在哪些问题
- 对代码的扩展性不友好,比如随着业务扩展,单例的链接池需要将读写分开为两个链接池,单例对这种扩展性的需求支持不友好,会出现大量代码的改动,不符合开闭原则。
- 对代码的可测试性不友好,因为是全局的变量,改动后会影响到其他类方法行为的更改