Ensure a class only has one instance, and provide a global point of access to it
保证一个类只有一个实例,并且为它提供一个全局访问点。

如何判断一个对象是否应该被建模成单例?通常,被建模成单例的对象都有“中心点”的含义,
比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,是一个中心点吗?
根据单例模式的定义,实现的关键点有两个:

  1. 限制调用者直接实例化该对象;
  2. 为该对象的单例提供一个全局唯一的访问方法。

单例模式可分为饿汉模式和懒汉模式,前者是在系统初始化期间完成单例对象的实例化,后者则是在调用时才进行实例化, 从而节约内存,但是存在并发安全的问题(go中使用sync.Once)

单例模式也可以实现多态,如果你预测该单例未来可能会扩展,那么就可以将它设计成抽象的接口,让客户端依赖抽象,这样,未来扩展时就无需改动客户端程序了。

懒汉模式

在被调用时进行初始化,主要是要注意并发问题,使用Once.Do来保证

  1. type network struct {...}
  2. // 单例
  3. var instance *network
  4. // 定义 once 对象
  5. var once = sync.Once{}
  6. // 通过once对象确保instance只被初始化一次
  7. func Instance() *network {
  8. once.Do(func() {
  9. // 只会被调用一次
  10. instance = &network{sockets: sync.Map{}}
  11. })
  12. return instance
  13. }

饿汉模式

在程序一开始就初始化

典型使用场景

  1. 日志。每个服务通常都会需要一个全局的日志对象来记录本服务产生的日志。
  2. 全局配置。对于一些全局的配置,可以通过定义一个单例来供客户端使用。
  3. 唯一序列号生成。唯一序列号生成必然要求整系统只能有一个生成实例,非常合适使用单例模式。
  4. 线程池、对象池、连接池等。xxx池的本质就是共享,也是单例模式的常见场景。
  5. 全局缓存
    ……

缺点:
单例模式本质是全局变量,具有全局变量的缺点

  1. 函数调用的隐式耦合。通常我们都期望从函数的声明中就能知道该函数做了什么、依赖了什么、返回了什么。使用使用单例模式就意味着,无需通过函数传参,就能够在函数中使用该实例。也即将依赖/耦合隐式化了,不利于更好地理解代码。
  2. 对测试不友好。通常对一个方法/函数进行测试,我们并不需要知道它的具体实现。但如果方法/函数中有使用单例对象,我们就不得不考虑单例状态的变化了,也即需要考虑方法/函数的具体实现了。
  3. 并发问题。共享就意味着可能存在并发问题,我们不仅需要在初始化阶段考虑并发问题,在初始化后更是要时刻注意。因此,在高并发的场景,单例模式也可能存在锁冲突问题。