原理
一个类只允许创建一个对象,那么这个类就是单例类
代码实现
饿汉式
在程序启动时创建唯一的Logger对象
var loggerCreateOnce sync.Once
var singleLogger *Logger
func newSingleLogger() {
filePath := "/log.txt"
f, _ := os.Create(filePath)
writer := bufio.NewWriter(f)
singleLogger = &Logger{fileWriter: writer}
}
// 饿汉式,直接在main启动时调用该方法进行对象初始化,然后调用GetLoggerInstance获取对象
func InitLogger() {
loggerCreateOnce.Do(newSingleLogger)
}
func GetLoggerInstance() *Logger {
return singleLogger
}
懒汉式
使用Logger对象时创建唯一的Logger对象
var loggerCreateOnce sync.Once
var singleLogger *Logger
func newSingleLogger() {
filePath := "/log.txt"
f, _ := os.Create(filePath)
writer := bufio.NewWriter(f)
singleLogger = &Logger{fileWriter: writer}
}
//懒汉式, 需要使用对象时调用
func GetLoggerInstance() *Logger {
loggerCreateOnce.Do(newSingleLogger)
return singleLogger
}
应用场景
解决资源访问冲突问题
例如,解决日志文件并发写入问题
未使用单例模式前的代码如下:
type Logger struct {
fileWriter *bufio.Writer
}
func newLogger(filePath string) *Logger {
f, _ := os.Create(filePath)
writer := bufio.NewWriter(f)
return &Logger{
fileWriter: writer,
}
}
func (l *Logger) info(msg string) {
l.fileWriter.WriteString(msg)
}
type UserService struct {
logger *Logger
}
func NewUserService() *UserService {
return &UserService{
logger: newLogger("/logger.txt"),
}
}
func (s *UserService) Login() {
// 记录日志
s.logger.info("login")
}
由于每次使用logger时都创建了一个新的logger对象使用,所以会有并发写入覆盖的问题,时间线如下图:
线程1刚在91起始位置写入abc,线程2又在91起始位置写入sdf,线程2写入的数据覆盖了线程1写入的数据
采用单例模式后的代码:
func NewUserService() *UserService {
return &UserService{
logger: GetLoggerInstance(), //这里调用的就是懒汉式的单例模式
}
}
func (s *UserService) Login() {
// 记录日志
s.logger.info("login")
}
可以采用单例模式,这样多个线程使用的都是同一个logger对象,同一个logger对象在写入时自带对象锁,就不会产生资源并发访问冲突问题
表示全局唯一类(比如配置类、工厂类)
比如需要一个 ID自增生成器,那么每个线程肯定需要使用同一个生成器才能保证ID不会重复,所以ID生成器类就适合使用单例模式
代码实现
type IDGenerate struct {
id int32
}
var idGenerateCreateOnce sync.Once
var singleIDGenerate *IDGenerate
func GetIDGenerateInstance() *IDGenerate {
idGenerateCreateOnce.Do(func() {
singleIDGenerate = new(IDGenerate)
})
return singleIDGenerate
}
func (g *IDGenerate) GetID() int32 {
g.id = atomic.AddInt32(&g.id, 1)
return g.id
}
扩展
实现进程间单例
借助外部存储,每次使用对象时从外部获取,使用完后,在放回外部
对象序列化成字符串后存储到外部存储
为了保证只有一个进程操作单例对象,采用分布式锁保证;为了保证一个进程内只有一个线程操作单例对象,采用Mutex锁保证
IDGenerate进程间单例,代码实现
//ID generate
type IDGenerate struct {
id int32
}
var mu sync.Mutex
var distributeLock DistributeLock
type IDGenerateStory interface {
Save(generator *IDGenerate)
Get() *IDGenerate
}
type DistributeLock interface {
Lock() bool
Unlock() bool
}
func GetMultiProgressIDGeneratorInstance() *IDGenerate {
res := new(IDGenerate)
mu.Lock()
for {
if distributeLock.Lock() {
var story IDGenerateStory
res = story.Get()
break
}
}
return res
}
func RefreshMultiProgressIDGeneratorInstance(idGenerate *IDGenerate) {
var story IDGenerateStory
story.Save(idGenerate)
distributeLock.Unlock()
mu.Unlock()
}
一些总结
- 如果对象的创建过程非常简单,那么可以直接写在sync.Once的Do方法中,比如IDGenerate;如果对象的创建过程比较复杂,那么还是单独写一个newXXX方法,将创建过程封装在new方法中
- 如果不希望提前加载对象那么使用懒汉式;如果加载对象比较慢,不希望使用的时候由于加载对象而导致时延增加,那么使用饿汉式在程序启动时直接加载完成