一文搞懂单例模式

原理

一个类只允许创建一个对象,那么这个类就是单例类

代码实现

饿汉式

在程序启动时创建唯一的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张图片
线程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()
}

一些总结

  1. 如果对象的创建过程非常简单,那么可以直接写在sync.Once的Do方法中,比如IDGenerate;如果对象的创建过程比较复杂,那么还是单独写一个newXXX方法,将创建过程封装在new方法中
  2. 如果不希望提前加载对象那么使用懒汉式;如果加载对象比较慢,不希望使用的时候由于加载对象而导致时延增加,那么使用饿汉式在程序启动时直接加载完成

你可能感兴趣的:(基础知识,设计模式,go)