golang单例模式

299 阅读2分钟

单例模式是创建型模式之一,保证每个类只有一个实例,因此节省内存,加快对象访问速度,对象访问被公用的情况适合使用,如:

  • 配置文件的初始化,一个进程中的所有线程公用一份配置数据。
  • 链接池类的初始化,一般小的项目中只有一个链接池
  • 日志,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()
}

存在哪些问题

  • 对代码的扩展性不友好,比如随着业务扩展,单例的链接池需要将读写分开为两个链接池,单例对这种扩展性的需求支持不友好,会出现大量代码的改动,不符合开闭原则。
  • 对代码的可测试性不友好,因为是全局的变量,改动后会影响到其他类方法行为的更改