在面向对象编程语言中,单例模式(Singleton pattern)确保一个类只有一个实例,并提供对该实例的全局访问。
那么Go语言中,单例模式确认一个类型只有一个实例,并提供对改实例的全局访问,一般就是直接访问全局变量即可。
比如Go标准库中的os.Stdin、os.Stdout、os.Stderr分别代表标准输入、标准输出和标准错误输出。它们是*os.File类型的全局变量,可以在程序中直接使用:
1 | var ( |
又比如io包下的EOF:
1 | var EOF = errors.New("EOF") |
Go标准库中有很多这样的单例的实现,又比如http.DefaultClient、http.DefaultServeMux、http.DefaultTransport、net.IPv4zero都是单例对象。
有时候,有人也认为是单例模式也是反模式。
反模式(Anti-pattern)是一种在软件工程中常见的概念,主要指在软件设计、开发中要避免使用的模式或实践。
反模式的一些主要特征包括:
- 它通常是初学者常犯的错误或陷阱。
- 它反映了一种看似可行但实际上低效或错误的解决方案。
- 使用反模式可能在短期内出现类似解决问题的效果,但长期来看会适得其反。
- 它通常是一个坏的或劣质的设计,不符合最佳实践。
- 存在一个更好的、可替代的解决方案。
一些常见的反模式示例:
- 复制-粘贴编程:为了重复使用代码,直接复制粘贴,而不创建函数或模块。
- 上帝对象:一个巨大的包含全部功能的复杂对象。
- 依赖注入滥用:即使简单的对象也进行依赖注入,增加了复杂性。
- 自我封装:通过封装无谓的细节来增加类的复杂性。
- 过度抽象和设计:代码缺乏可读性
为什么这么说呢,加入两个goroutine同时使用http.DefaultClient, 其中一个goroutine修改了这个client的一些字段,也会影响到第二个goroutine的使用。
而且这些单例都是可修改对象,第三库甚至偷偷修改了这个变量的值,你都不会发现,比如你想连接本地的53端口,查询一些域名,但是可能被别人劫持到它的服务器上:
1 | package main |
本来我想查询本机的dns服务器,结果却被劫持到谷歌的8.8.8.8 DNS服务器上进行查询了。
惰性初始模式(Lazy initialization, 懒汉式初始化)推迟对象的创建、数据的计算等需要耗费较多资源的操作,只有在第一次访问的时候才执行。惰性初始是一种拖延战术。在第一次需求出现以前,先延迟创建对象、计算值或其它昂贵的代码片段。
一句话,也就是延迟初始化。
如果你是Java程序员,面试的时候大概率会被问到单例的模式的实现,就像问茴香豆的茴字有几个写法。Java中大概有下面几种单例的实现:
饿汉式(Eager Initialization)
懒汉式(Lazy Initialization)
双重检查锁(Double-Checked Locking)
静态内部类(Static Inner Class)
枚举单例(Enum Singleton)
后面四种都属于惰性初始模式,在实例被第一次使用才会初始化。
Rust语言中常使用lazy_static 宏来实现惰性初始模式实现单例:
1 | lazy_static! { |
而在Go标准库中,可以使用sync.Once来实现惰性初始单例模式。比如os/user获取当前用户的时候,只需执行一次耗时的系统调用,后续就直接从第一次初始化的结果中获取,即使第一次查询失败:
1 | func Current() (*User, error) { |
在即将发布的Go 1.21中,sync.Once又多了三个兄弟:
1 | func OnceFunc(f func()) func() |
它们是基于sync.Once实现的辅助函数,比如Current就可以使用OnceValues改写,有兴趣的同学可以试试。
这三个新函数的讲解可以阅读我先前的一篇文章:sync.Once的新扩展 (colobu.com)
