1. 簡介
本文介紹使用sync.Once來實作單例模式,包括單例模式的定義,以及使用sync.Once實作單例模式的示例,同時也比較了其他單例模式的實作,最后以一個開源框架中使用sync.Once實作單例模式的例子來作為結尾,
2. 基本實作
2.1 單例模式定義
單例模式是一種創建型設計模式,它保證一個類只有一個實體,并提供一個全域訪問點來訪問這個實體,在整個應用程式中,所有對于這個類的訪問都將回傳同一個實體物件,
2.2 sync.Once實作單例模式
下面是一個簡單的示例代碼,使用 sync.Once 實作單例模式:
package singleton
import "sync"
type singleton struct {
// 單例物件的狀態
}
var (
instance *singleton
once sync.Once
)
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
// 初始化單例物件的狀態
})
return instance
}
在上面的示例代碼中,我們定義了一個 singleton 結構體表示單例物件的狀態,然后將它的實體作為一個包級別的變數 instance,并使用一個 once 變數來保證 GetInstance 函式只被執行一次,
在 GetInstance 函式中,我們使用 once.Do 方法來執行一個初始化單例物件,由于 once.Do 方法是基于原子操作實作的,因此可以保證并發安全,即使有多個協程同時呼叫 GetInstance 函式,最終也只會創建一個物件,
2.3 其他方式實作單例模式
2.3.1 全域變數定義時賦值,實作單例模式
在 Go 語言中,全域變數會在程式啟動時自動初始化,因此,如果在定義全域變數時給它賦值,則物件的創建也會在程式啟動時完成,可以通過此來實作單例模式,以下是一個示例代碼:
type MySingleton struct {
// 欄位定義
}
var mySingletonInstance = &MySingleton{
// 初始化欄位
}
func GetMySingletonInstance() *MySingleton {
return mySingletonInstance
}
在上面的代碼中,我們定義了一個全域變數 mySingletonInstance 并在定義時進行了賦值,從而在程式啟動時完成了物件的創建和初始化,在 GetMySingletonInstance 函式中,我們可以直接回傳全域變數 mySingletonInstance,從而實作單例模式,
2.3.2 init 函式實作單例模式
在 Go 語言中,我們可以使用 init 函式來實作單例模式,init 函式是在包被加載時自動執行的函式,因此我們可以在其中創建并初始化單例物件,從而保證在程式啟動時就完成物件的創建,以下是一個示例代碼:
package main
type MySingleton struct {
// 欄位定義
}
var mySingletonInstance *MySingleton
func init() {
mySingletonInstance = &MySingleton{
// 初始化欄位
}
}
func GetMySingletonInstance() *MySingleton {
return mySingletonInstance
}
在上面的代碼中,我們定義了一個包級別的全域變數 mySingletonInstance,并在 init 函式中創建并初始化了該物件,在 GetMySingletonInstance 函式中,我們直接回傳該全域變數,從而實作單例模式,
2.3.3 使用互斥鎖實作單例模式
在 Go 語言中,可以只使用一個互斥鎖來實作單例模式,下面是一個簡單代碼的演示:
var instance *MySingleton
var mu sync.Mutex
func GetMySingletonInstance() *MySingleton {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &MySingleton{
// 初始化欄位
}
}
return instance
}
在上面的代碼中,我們使用了一個全域變數instance來存盤單例物件,并使用了一個互斥鎖 mu 來保證物件的創建和初始化,具體地,我們在 GetMySingletonInstance 函式中首先加鎖,然后判斷 instance 是否已經被創建,如果未被創建,則創建并初始化物件,最后,我們釋放鎖并回傳單例物件,
需要注意的是,在并發高的情況下,使用一個互斥鎖來實作單例模式可能會導致性能問題,因為在一個 goroutine 獲得鎖并創建物件時,其他的 goroutine 都需要等待,這可能會導致程式變慢,
2.4 使用sync.Once實作單例模式的優點
相對于init 方法和使用全域變數定義賦值單例模式的實作,sync.Once 實作單例模式可以實作延遲初始化,即在第一次使用單例物件時才進行創建和初始化,這可以避免在程式啟動時就進行物件的創建和初始化,以及可能造成的資源的浪費,
而相對于使用互斥鎖實作單例模式,使用 sync.Once 實作單例模式的優點在于更為簡單和高效,sync.Once提供了一個簡單的介面,只需要傳遞一個初始化函式即可,相比互斥鎖實作方式需要手動處理鎖、判斷等操作,使用起來更加方便,而且使用互斥鎖實作單例模式需要在每次訪問單例物件時進行加鎖和解鎖操作,這會增加額外的開銷,而使用 sync.Once 實作單例模式則可以避免這些開銷,只需要在第一次訪問單例物件時進行一次初始化操作即可,
但是也不是說sync.Once便適合所有的場景,這個是需要具體情況具體分析的,下面說明sync.Once和init方法,在哪些場景下使用init更好,在哪些場景下使用sync.Once更好,
2.5 sync.Once和init方法適用場景
對于init實作單例,比較適用于在程式啟動時就需要初始化變數的場景,因為init函式是在程式運行前執行的,可以確保變數在程式運行時已經被初始化,
對于需要延遲初始化某些物件,物件被創建出來并不會被馬上使用,或者可能用不到,例如創建資料庫連接池等,這時候使用sync.Once就非常合適,它可以保證物件只被初始化一次,并且在需要使用時才會被創建,避免不必要的資源浪費,
3. gin中單例模式的使用
3.1 背景
這里首先需要介紹下gin.Engine, gin.Engine是Gin框架的核心組件,負責處理HTTP請求,路由請求到對應的處理器,處理器可以是中間件、控制器或處理HTTP回應等,每個gin.Engine實體都擁有自己的路由表、中間件堆疊和其他配置項,通過呼叫其方法可以注冊路由、中間件、處理函式等,
一個HTTP服務器,只會存在一個對應的gin.Engine實體,其保存了路由映射規則等內容,
為了簡化開發者Gin框架的使用,不需要用戶創建gin.Engine實體,便能夠完成路由的注冊等操作,提高代碼的可讀性和可維護性,避免重復代碼的出現,這里對于一些常用的功能,抽取出一些函式來使用,函式簽名如下:
// ginS/gins.go
// 加載HTML模版檔案
func LoadHTMLGlob(pattern string) {}
// 注冊POST請求處理器
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 注冊GET請求處理器
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {}
// 啟動一個HTTP服務器
func Run(addr ...string) (err error) {}
// 等等...
接下來需要對這些函式來進行實作,
3.2 具體實作
首先從使用出發,這里使用POST方法/GET方法注冊請求處理器,然后使用Run方法啟動服務器:
func main() {
// 注冊url對應的處理器
POST("/login", func(c *gin.Context) {})
// 注冊url對應的處理器
GET("/hello", func(c *gin.Context) {})
// 啟動服務
Run(":8080")
}
這里我們想要的效果,應該是呼叫Run方法啟動服務后,往/login路徑發送請求,此時應該執行我們注冊的對應處理器,往/hello路徑發送請求也是同理,
所以,這里POST方法,GET方法,Run方法應該都是對同一個gin.Engine 進行操作的,而不是各自使用各自的gin.Engine實體,亦或者每次呼叫就創建一個gin.Engine實體,這樣子才能達到我們預想的效果,
所以,我們需要實作一個方法,獲取gin.Engine實體,每次呼叫該方法都是獲取到同一個實體,這個其實也就是單例的定義,然后POST方法,GET方法又或者是Run方法,呼叫該方法獲取到gin.Engine實體,然后呼叫實體去呼叫對應的方法,完成url處理器的注冊或者是服務的啟動,這樣子就能夠保證是使用同一個gin.Engine實體了,具體實作如下:
// ginS/gins.go
import (
"github.com/gin-gonic/gin"
)
var once sync.Once
var internalEngine *gin.Engine
func engine() *gin.Engine {
once.Do(func() {
internalEngine = gin.Default()
})
return internalEngine
}
// POST is a shortcut for router.Handle("POST", path, handle)
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().POST(relativePath, handlers...)
}
// GET is a shortcut for router.Handle("GET", path, handle)
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().GET(relativePath, handlers...)
}
這里engine() 方法使用了 sync.Once 實作單例模式,確保每次呼叫該方法回傳的都是同一個 gin.Engine 實體,然后POST/GET/Run方法通過該方法獲取到gin.Engine實體,然后呼叫實體中對應的方法來完成對應的功能,從而達到POST/GET/Run等方法都是使用同一個實體操作的效果,
3.3 sync.Once實作單例的好處
這里想要達到的目的,其實是GET/POST/Run等抽取出來的函式,使用同一個gin.Engine實體,
為了達到這個目的,我們其實可以在定義internalEngine 變數時,便對其進行賦值;或者是通init函式完成對internalEngine變數的賦值,其實都可以,
但是我們抽取出來的函式,用戶并不一定使用,定義時便初始化或者在init方法中便完成了對變數的賦值,用戶沒使用的話,創建出來的gin.Engine實體沒有實際用途,造成了不必要的資源的浪費,
而engine方法使用sync.Once實作了internalEngin的延遲初始化,只有在真正使用到internalEngine時,才會對其進行初始化,避免了不必要的資源的浪費,
這里其實也印證了上面我們所說的sync.Once的適用場景,對于不會馬上使用的單例物件,此時可以使用sync.Once來實作,
4.總結
單例模式是一種常用的設計模式,用于保證一個類僅有一個實體,在單例模式中,常常使用互斥鎖或者變數賦值的方式來實作單例,然而,使用sync.Once可以更方便地實作單例,同時也能夠避免了不必要的資源浪費,當然,沒有任何一種實作是適合所有場景的,我們需要根據具體場景具體分析,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/547126.html
標籤:其他
