在面向对象编程语言中,单例模式(Singleton pattern)确保一个类只有一个实例,并提供对该实例的全局访问。
那么Go语言中,单例模式确认一个类型只有一个实例,并提供对改实例的全局访问,一般就是直接访问全局变量即可。
比如Go标准库中的os.Stdin
、os.Stdout
、os.Stderr
分别代表标准输入、标准输出和标准错误输出。它们是*os.File
类型的全局变量,可以在程序中直接使用:
1 2 3 4 5
| var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") )
|
又比如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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| package main import ( "fmt" "net" "github.com/miekg/dns" ) func main() { net.IPv4zero = net.IPv4(8, 8, 8, 8) dnsServer := net.JoinHostPort(net.IPv4zero.String(), "53") c := new(dns.Client) msg := new(dns.Msg) msg.SetQuestion(dns.Fqdn("rpcx.io"), dns.TypeA) resp, _, err := c.Exchange(msg, dnsServer) if err != nil { fmt.Println("Error sending DNS request:", err) return } 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
宏来实现惰性初始模式实现单例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| lazy_static! { static ref SINGLETON: Mutex<Singleton> = Mutex::new(Singleton::new()); } struct Singleton { } impl Singleton { fn new() -> Self { Singleton { } } }
|
而在Go标准库中,可以使用sync.Once
来实现惰性初始单例模式。比如os/user
获取当前用户的时候,只需执行一次耗时的系统调用,后续就直接从第一次初始化的结果中获取,即使第一次查询失败:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func Current() (*User, error) { cache.Do(func() { cache.u, cache.err = current() }) if cache.err != nil { return nil, cache.err } u := *cache.u return &u, nil } var cache struct { sync.Once u *User err error }
|
在即将发布的Go 1.21中,sync.Once又多了三个兄弟:
1 2 3
| 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)