单例模式是日常用到、常见的一种设计模式,但是其中有些细节还是值得说一说:
核心思想
- 全局只有一个实例
- 禁止外部直接创建对象
- 提供统一入口获取实例
- 多线程下必须安全
Go 单例模式实现
go 因为不是传统的面向对象语言,它没有类,也没有构造函数这种说法,这里最重要的是要解决封装的问题。
比如 java 的话,我们可以通过把类的构造函数改成私有的,从而达到禁止 new 的目的,这样类的使用者无法通过 new 来新建单例类的实例。
虽然无法通过 new 来创建实例,但是语言留了个口子,可以通过 static 来获取实例,这样单例类的创建者就可以控制该类的实例化方法了。
传统的 java 方案如下:
public class Singleton {// 禁止指令重排private static volatile Singleton instance;// 私有构造,禁止newprivate Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一层:避免每次加锁,提升性能synchronized (Singleton.class) {if (instance == null) { // 第二层:防止并发穿透,保证单例instance = new Singleton();}}}return instance;}
}
但是 java 没有类,也没有 static 的方式,那 go 要怎么处理呢?
实际上,go 可以用 结构体小写 + 全局变量 实现封装。
- 结构体小写相当于构造函数小写禁止了 new
- 全局变量相当于 static,虽然作用范围是包内,但是实际上它不是整个项目级别的全局变量,所以这里有点要强调的是 golang 的全局变量和 C 的全局变量还不太一样,它是介于 java static 变量和 C 全局变量之间的一种形式。
另外 go 标准库天然支持了 sync.Once 对并发友好,可以直接用:
package mainimport ("fmt""sync"
)// 定义单例结构体(首字母小写,外部无法直接创建,强制使用 GetInstance())
type singleton struct {// 业务字段Name string
}// 私有全局实例(首字母小写,外部无法直接访问)
var instance *singleton// 初始化执行器,保证只执行一次
var once sync.Once// GetInstance 公开方法:获取单例实例(唯一入口)
func GetInstance() *singleton {// 只会执行一次初始化once.Do(func() {fmt.Println("创建单例实例(仅执行一次)")instance = &singleton{Name: "我是全局唯一单例",}})return instance
}// 测试:多协程同时获取,验证只创建一次
func main() {var wg sync.WaitGroup// 开启 10 个协程同时获取单例for i := 0; i < 10; i++ {wg.Add(1)go func(num int) {defer wg.Done()ins := GetInstance()fmt.Printf("协程 %d:实例地址 = %p\n", num, ins)}(i)}wg.Wait()
}
其他传统写法:
package mainimport ("fmt""sync"
)type singleton struct {Name string
}var instance *singleton
var mu sync.Mutexfunc GetInstance() *singleton {// 加锁,防止多协程同时创建mu.Lock()defer mu.Unlock()if instance == nil {instance = &singleton{Name: "加锁懒汉式单例"}}return instance
}func main() {fmt.Println(GetInstance() == GetInstance()) // true
}
