如何用 Go 寫出優美的代碼 - Go 的設計模式【單例模式,工廠方法模式】篇一
大家好,我是追麾 (hui)。
接下來的幾周時間給大家分享一系列 Go 設計模式文章,設計模式在我們的面試中也會被經常問到,像 Java 語言會用到設計模式,對於 Go 語言,設計模式使用會比較弱點,所以這裏給大家一起來學習分享 Go 的設計模式,讓我們在開發中也大量應用到設計模式,幫助我們的 Go 代碼更加優美,可讀性更好。
第一篇主要分享兩種模式,單例模式和工廠方法模式。
Go 的單例模式
單例模式定義:Ensure a class has only one instance, and provide a global point of access to it.(確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。)
單例模式優缺點
-
優點:
-
減少內存開支,單例模式在內存中只有一個實例,特別是一個對象需要頻繁的創建,銷燬時,而且創建或者銷燬時性能又無法優化,單例模式有非常明顯的優勢。
-
減少系統性能的開銷,當一個對象的產生需要多比較多的資源時,可以通過應用程序啓動時直接產生一個單例對象,永久駐留內存。單例模式也可以在系統設置全局的訪問點,優化和共享資源訪問。
-
避免對資源的多重佔用,比如 redis 連接池對象,mysql 連接池對象實現都可以避免同一個資源被同時操作。
-
缺點:
-
代碼擴展不方便,單例模式一般沒有接口,擴展很困難,單例模式爲什麼不能增加接口呢?因爲接口對單例模式是沒有任何意義的,它要求 “自行實例化”,並且提供單一實例、接口或抽象類是不可能被實例化的。
-
單例模式與單一職責原則有衝突,一個類應該只實現一個邏輯,而不關心它是否是單例的,是不是要單例取決於環境。
-
對代碼的可測性不好:如果是修改全局變量,測試的時候還要注意不同的測試用例對它的修改問題。
-
會隱藏類之間的依賴關係:降低可讀性,如果通過構造函數,參數傳遞等方式聲明類之間的依賴關係,我們可以通過查看函數定義,就能容易識別出來。
單例模式的應用場景
-
對目標實例使用是一致性的需求,即所有的客戶端使用共享的實例,這樣這個類就可以只有一個實例,並通過單例模式實現。例如一個軟件配置信息封裝在一個類中,這個類對所有客戶端提供一致的配置信息,若所有客戶端使用共享的目標類實例,就能保證該實例服務的一致性。
-
對象實例化代價需求,當一個類的實例化需要付出昂貴的代價(指實例化所需的時間或資源)時,而該對象向所有客戶端提供的又是無狀態服務或者提供的服務與實例狀態無關。減少目標類多次實例化的代價(即不需要每個客戶端在使用時都進行目標類的對象實例化),可以起到優化程序的作用。
單例模式實現方式
懶漢式:使用的時候才進行初始化,即懶加載
-
優點:只在第一次使用的時候調用,一定程度上節省了資源
-
缺點:第一次加載需要進行及時實例化,反應稍慢,最大的問題是每次調用 GetInstance 都進行同步,會造成不少開銷 我們通過 Go 示例來看下懶漢式的實現:
package main
func main() {
// 調用實例對象
GetInstance()
}
type Instance struct {
}
// 實例全局對象
var lazyInstance *Instance
func GetInstance() *Instance {
// 判斷第一次如果爲空,則給lazy對象重新賦值。
if lazyInstance == nil {
lazyInstance = &Instance{}
}
return lazyInstance
}
在 Go 語言中,通過懶漢式來實現單例,重要點在判斷第一次實例化的對象爲空,則給對象重新賦值返回,這裏如果每次調用 GetInstance,出現併發問題,則會造成不少開銷,像這種問題主要是使用雙重檢測來解決。
餓漢式:在程序初始化的時候或者類加載的時候就已經創建好對象,加載速度快。
-
優點:實現簡單,執行效率高,線程安全
-
缺點:程序初始化或者類加載時就初始化實例,可能佔用不必要內存浪費
我們通過 Go 示例來看下餓漢式實現:
package main
func main() {
GetInstance()
}
//對象
type Instance struct {
}
var lazyInstance *Instance
// 餓漢式:在程序初始化的時候或者類加載的時候就已經創建好對象,加載速度快。
func init() {
lazyInstance = &Instance{}
}
// 餓漢式調用
func GetInstance() *Instance {
return lazyInstance
}
在 Go 語言中,通過餓漢式來實現單例主要依賴 init 函數初始化變量,或者自己定義函數(函數內部初始化變量)在初始化程序或者加載包之前初始化,然後定義一個通用的方法對外服務。
雙重校驗:在懶漢式的基礎上,加上類級別的鎖。在 Go 語言中主要是基於 package 做全局鎖。
我們通過 Go 示例來看下雙重校驗實現:
package main
import "sync"
func main() {
// 調用實例對象
GetInstance()
}
type Instance struct {
}
// 實例全局對象
var lazyInstance *Instance
// 包級別的鎖
var once = &sync.Once{}
func GetInstance() *Instance {
// 判斷第一次如果爲空,則給lazy對象重新賦值。
if lazyInstance == nil {
// once.Do只執行一次
once.Do(func() {
lazyInstance = &Instance{}
})
}
return lazyInstance
}
靜態變量(Go 裏面通過常量來實現):Java 中利用了靜態內部類延遲初始化的特性,來達到與雙重校驗鎖方式一樣的功能,像 Go 中只能通過常量來實現,但是 Go 的常量僅支持整型,浮點型,字符串,bool 值,所以在 Go 中實現常量實現單例模式沒什麼意義。
枚舉類:該方式利用了枚舉類的特性,不僅能避免線程同步問題,還防止反序列化重新創建新的對象。在 Java 中是能實現,但是 Go 中是沒有這種,無法實現單例。
Go 的工廠方法模式
工廠方法模式定義:工廠方法(Factory Method)類定義產品對象創建接口,但由子類實現具體產品對象的創建。
工廠方法模式優缺點
-
優點:
-
良好的封裝性,代碼結構清晰:一個對象創建是有條件約束的,如一個調用者需要一個具體的產品對象,只要知道這個產品的類名(或約束字符串)就可以了,不用知道創建對象的艱辛過程,降低模塊間的耦合。
-
工廠方法模式的擴展性非常優秀:在增加產品類的情況下,只要適當地修改具體的工廠類或擴展一個工廠類,就可以完成擴展而擁抱變化。
-
屏蔽產品類:產品類的實現如何變化,調用者都不需要關心,它只需要關心產品的接口,只要接口保持不變,系統中的上層模塊就不發生變化,符合開閉原則。
-
典型的解耦框架:高層模塊只需要知道產品的抽象類,其他的實現類都不用關心,符合迪米特法則,我不需要的就不要去交流;也符合依賴倒置原則,只依賴產品類的抽象;當然也符合里氏替換原則,使用產品子類替換產品父類。
-
缺點:
-
增加系統複雜性:當我們新增產品時,還需要對應工廠類,系統中類的個數會成倍增加,相當於系統複雜性。
工廠方法模式應用場景
-
當業務類處理產品對象時,無法知道產品對象的具體類型,或不需要知道產品對象的具體類型(產品具有不同的子類)。
-
當業務類處理不同的產品子類對象業務時,希望由自己的子類實現產品子類對象的創建。
工廠方法模式實現方式
在 Go 語言中,需要實現工廠方法模式,則需要使用到接口,定義一些公共的方法,用不同的 struct 對象來實現接口裏面公共的方法,struct 本身的對象的具體類型是不知道的,只有對象本身才知道。下面我們具體來看一個示例。
在很多聚合廣告業務中,我們需要調用不同的廣告廠商的接口,然後通過不同的廣告廠商的數據存到自己的業務系統中,這個時候就可以使用工廠方法模式來實現,首先一些公共的方法 RequestThirdApi(),每個廠商都要實現這些方法,每個廠商本身需要先按照自身的廣告廠商接口組裝請求數據,然後去請求,請求完了之後不同廠商接口返回的結構是不一樣的,結果不一樣,則可以通過處理成同樣結構的數據到我們自己的系統。下面我們來看下具體實現代碼:
package main
import "fmt"
func main() {
NewFactory("kuaishou").RequestThirdApi()
}
// 做廣告投放需要拉取不同的廣告廠商,不同的廣告廠商是不一樣,但是都會存在請求第三方的方法
type ThirdResponseData struct {
}
// 工廠方法接口
type HttpRequestFactory interface {
RequestThirdApi() *ThirdResponseData
}
// 頭條廠商 工廠類
type TouTiaoFactory struct {
}
func (t *TouTiaoFactory) RequestThirdApi() *ThirdResponseData {
// 組裝請求
// 執行請求
// 處理請求響應結果,處理成ThirdResponseData這個的返回結構
fmt.Println("頭條")
return &ThirdResponseData{}
}
// 快手廠商 工廠類
type KuaiShouFactory struct {
}
func (t *KuaiShouFactory) RequestThirdApi() *ThirdResponseData {
// 組裝請求
// 執行請求
// 處理請求響應結果,處理成ThirdResponseData這個的返回結構
fmt.Println("快手")
return &ThirdResponseData{}
}
// 用一個簡單工廠封裝工廠方法,這個是入口函數
func NewFactory(f string) HttpRequestFactory {
switch f {
case "kuaishou":
return &KuaiShouFactory{}
case "toutiao":
return &TouTiaoFactory{}
}
return nil
}
上面只是一個簡單的版本,實際比這個複雜,我們會處理組裝請求,執行請求,響應都會用不同的方法來實現,這樣會看起來比較清晰。
關於 Go 的設計模式第一篇,就先分享到這裏。
參考文獻
《軟件設計模式之禪》
最後
如果這篇文章對您有所幫助,或者有所啓發的話,求一鍵三連:點贊、轉發、在看,您的支持是我堅持寫作最大的動力。
利志分享 分享技術,職場,人生感悟等,專注架構,go,k8s,kafka,clickhouse,個人小站:bbs.zengzhihai.com
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5C1R2p-uug3ANV9pMAG3qA