Go 單例模式和惰性初始化模式
在面向對象編程語言中,單例模式 (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]
參考資料
[1]
sync.Once 的新擴展 (colobu.com): https://colobu.com/2023/05/29/extends-sync-Once/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NQOmlQfFpM-asN9I9CeVkw