Go 單例模式和惰性初始化模式

在面向對象編程語言中,單例模式 (Singleton pattern) 確保一個類只有一個實例,並提供對該實例的全局訪問。

那麼 Go 語言中,單例模式確認一個類型只有一個實例,並提供對該實例的全局訪問,一般就是直接訪問全局變量即可。

比如 Go 標準庫中的os.Stdinos.Stdoutos.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.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 中大概有下面幾種單例的實現:

後面四種都屬於惰性初始模式,在實例被第一次使用纔會初始化。

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