简介
什么是单例模式?
单例模式是一种设计模式,其核心思想是确保某个类只有一个实例,并提供一个全局访问点。
本质
单例模式本质上就是全局变量。
优点
- 全局唯一实例:确保一个类只有一个实例,并提供全局访问点。
- 延迟初始化:实例只在需要时才会被创建。
- 配置加载较为方便:可以将初始化封装在使用过程中(个人觉得比较方便,省去了写一个统一的加载文件)
- 线程安全:使用
sync.Once
可以确保在多线程环境下也是安全的。
缺点
- 全局状态:单例模式本质上就是全局变量,可能导致不可预见的副作用和状态。
- 违反单一职责原则:除了控制实例创建和销毁,单例类通常还承担其他职责。
使用场景
- 数据库连接池:确保一个应用程序创建的数据库连接数量是有限和唯一的。
- 日志记录器:全局只需要一个日志记录对象即可。
- 配置管理:当需要从一个地方管理和访问配置信息时。
- 硬件接口访问:如打印机、图形卡等硬件资源通常需要全局唯一访问实例。
实现方式
懒汉式
最重要的一点,懒汉式可以避免手动 init,(自动 init 也不稳妥,go 的 init 的顺序比较迷)
同时懒汉式避免了一些多依赖初始化的问题(如果使用饿汉式,顺序就是一个很大的问题)
就懒汉式初次启动慢的问题,可能更适合一些长期运行的项目。
package models
import (
...
)
var db *gorm.DB
var once sync.Once
// todo 修改
func DB() *gorm.DB {
//使用 sync.Once 可以有效解决线程安全问题
once.Do(func() {
initDao()
})
return db
}
// initDao 连接数据库
func initDao() {
/* ... */
}
func main(){
//可以这样使用,成功把配置的初始化封装
DB().Create(...)
}
缺点
容易造成依赖循环
比如下方
var onceLog sync.Once
var log *zap.Logger{}
func Log(){
onceLog.Do(func() {
initLog()
})
return log
}
func initLog(){
/*而这里面需要从配置文件中读取 Log 日志的存储路径,以及来设置 log 的存储位置*/
}
var onceConf sync.Once
var conf *viper.Viper{}
func Config(){
onceLog.Do(func() {
initConf()
})
return conf
}
func initConf(){
/*这里面有个地方需要来加载日志的变化情况,此时需要用到 Log()*/
}
上面的设计显然是不合理的,当时不要把两段代码放在一起,单看可能挺合理的,我们需要时刻注意这个问题。
饿汉式
在类加载时就创建实例,确保实例的唯一性。
package main
import "fmt"
type Singleton struct{}
var instance = &Singleton{}
func GetInstance() *Singleton {
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
if s1 == s2 {
fmt.Println("s1 和 s2 是同一个实例")
} else {
fmt.Println("s1 和 s2 不是同一个实例")
}
}
优点
- 简单易实现:代码简单,易于理解。
- 线程安全:由于实例是在类加载时创建的,不存在多线程同步问题。
缺点
- 资源浪费:如果该实例一直没有被使用,会造成资源浪费。
- 可能存在初始化顺序问题:如果单例依赖其他类,可能会因为初始化顺序而导致问题。