在面向对象编程语言中,单例模式(Singleton pattern)确保一个类只有一个实例,并提供对该实例的全局访问。
那么 Go 语言中,单例模式确认一个类型只有一个实例,并提供对该实例的全局访问,一般就是直接访问全局变量即可。
比如 Go 标准库中的os.Stdin
、os.Stdout
、os.Stderr
分别代表标准输入、标准输出和标准错误输出。它们是*os.File
类型的全局变量,可以在程序中直接使用:
var (
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.DefaultClient
、http.DefaultServeMux
、http.DefaultTransport
、net.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)[1]
sync.Once 的新扩展 (colobu.com): https://colobu.com/2023/05/29/extends-sync-Once/
Go设计模式系列
真实世界的Go设计模式 - 建造者模式
真实世界的Go设计模式 - 工厂模式
Go可以使用设计模式,但绝不是《设计模式》中的那样