2

单例模式 和 惰性初始化模式

 9 months ago
source link: https://colobu.com/2023/07/27/go-design-patterns-singleton/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

单例模式 和 惰性初始化模式

在面向对象编程语言中,单例模式(Singleton pattern)确保一个类只有一个实例,并提供对该实例的全局访问。

那么Go语言中,单例模式确认一个类型只有一个实例,并提供对改实例的全局访问,一般就是直接访问全局变量即可。

比如Go标准库中的os.Stdinos.Stdoutos.Stderr分别代表标准输入、标准输出和标准错误输出。它们是*os.File类型的全局变量,可以在程序中直接使用:

Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")

又比如io包下的EOF:

var EOF = errors.New("EOF")

Go标准库中有很多这样的单例的实现,又比如http.DefaultClienthttp.DefaultServeMuxhttp.DefaultTransportnet.IPv4zero都是单例对象。

有时候,有人也认为是单例模式也是反模式。

反模式(Anti-pattern)是一种在软件工程中常见的概念,主要指在软件设计、开发中要避免使用的模式或实践。

反模式的一些主要特征包括:

  • 它通常是初学者常犯的错误或陷阱。
  • 它反映了一种看似可行但实际上低效或错误的解决方案。
  • 使用反模式可能在短期内出现类似解决问题的效果,但长期来看会适得其反。
  • 它通常是一个坏的或劣质的设计,不符合最佳实践。
  • 存在一个更好的、可替代的解决方案。

一些常见的反模式示例:

  • 复制-粘贴编程:为了重复使用代码,直接复制粘贴,而不创建函数或模块。
  • 上帝对象:一个巨大的包含全部功能的复杂对象。
  • 依赖注入滥用:即使简单的对象也进行依赖注入,增加了复杂性。
  • 自我封装:通过封装无谓的细节来增加类的复杂性。
  • 过度抽象和设计:代码缺乏可读性

为什么这么说呢,加入两个goroutine同时使用http.DefaultClient, 其中一个goroutine修改了这个client的一些字段,也会影响到第二个goroutine的使用。

而且这些单例都是可修改对象,第三库甚至偷偷修改了这个变量的值,你都不会发现,比如你想连接本地的53端口,查询一些域名,但是可能被别人劫持到它的服务器上:

package main
import (
"fmt"
"net"
"github.com/miekg/dns"
func main() {
// 单例对象被修改,实际可能在一个第三包的init函数中写了下面这一行
net.IPv4zero = net.IPv4(8, 8, 8, 8)
// 设置DNS服务器地址
dnsServer := net.JoinHostPort(net.IPv4zero.String(), "53")
// 创建DNS客户端
c := new(dns.Client)
// 构建DNS请求消息
msg := new(dns.Msg)
msg.SetQuestion(dns.Fqdn("rpcx.io"), dns.TypeA)
// 发送DNS请求消息
resp, _, err := c.Exchange(msg, dnsServer)
if err != nil {
fmt.Println("Error sending DNS request:", err)
return
// 解析DNS响应消息
ipAddr, err := parseDNSResponse(resp)
if err != nil {
fmt.Println("Error parsing DNS response:", err)
return
// 输出查询结果
fmt.Println("IPv4 Address for google.com:", ipAddr)
func parseDNSResponse(resp *dns.Msg) (string, error) {
if len(resp.Answer) == 0 {
return "", fmt.Errorf("No answer in DNS response")
for _, ans := range resp.Answer {
if a, ok := ans.(*dns.A); ok {
return a.A.String(), nil
return "", fmt.Errorf("No A record found in DNS response")

本来我想查询本机的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 宏来实现惰性初始模式实现单例:

lazy_static! {
static ref SINGLETON: Mutex<Singleton> = Mutex::new(Singleton::new());
struct Singleton {
// Add fields and methods as needed
impl Singleton {
fn new() -> Self {
Singleton {
// Initialize fields

而在Go标准库中,可以使用sync.Once来实现惰性初始单例模式。比如os/user获取当前用户的时候,只需执行一次耗时的系统调用,后续就直接从第一次初始化的结果中获取,即使第一次查询失败:

func Current() (*User, error) {
cache.Do(func() { cache.u, cache.err = current() })
if cache.err != nil {
return nil, cache.err
u := *cache.u // copy
return &u, nil
// cache of the current user
var cache struct {
sync.Once
u *User
err error

在即将发布的Go 1.21中,sync.Once又多了三个兄弟:

func OnceFunc(f func()) func()
func OnceValue(f func() T) func() T
func OnceValues(f func() (T1, T2)) func() (T1, T2)

它们是基于sync.Once实现的辅助函数,比如Current就可以使用OnceValues改写,有兴趣的同学可以试试。

这三个新函数的讲解可以阅读我先前的一篇文章:sync.Once的新扩展 (colobu.com)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK