最近使用 GRPC
發現一個設計特別好的地方,很是值得借鑑。golang
咱們在平常寫方法的時候,但願給某個字段設置一個默認值,不須要定製化的場景就不傳這個參數,可是 Golang
卻沒有提供像 PHP
、Python
這種動態語言設置方法參數默認值的能力。編程
以一個購物車舉例。好比我有下面這樣一個購物車的結構體,其中 CartExts
是擴展屬性,它有本身的默認值,使用者但願若是不改變默認值時就不傳該參數。可是因爲 Golang
沒法在參數中設置默認值,只有如下幾個選擇:安全
ext
字段都作爲參數,若是不須要的時候傳該類型的零值,這把複雜度暴露給調用者;ext
這個結構體作爲一個參數在初始化函數中,與 1
同樣,複雜度在於調用者;下面看下代碼具體會怎麼作app
const ( CommonCart = "common" BuyNowCart = "buyNow" ) type CartExts struct { CartType string TTL time.Duration } type DemoCart struct { UserID string ItemID string Sku int64 Ext CartExts } var DefaultExt = CartExts{ CartType: CommonCart, // 默認是普通購物車類型 TTL: time.Minute * 60, // 默認 60min 過時 } // 方式一:每一個擴展數據都作爲參數 func NewCart(userID string, Sku int64, TTL time.Duration, cartType string) *DemoCart { ext := DefaultExt if TTL > 0 { ext.TTL = TTL } if cartType == BuyNowCart { ext.CartType = cartType } return &DemoCart{ UserID: userID, Sku: Sku, Ext: ext, } } // 方式二:多個場景的獨立初始化函數;方式二會依賴一個基礎的函數 func NewCartScenes01(userID string, Sku int64, cartType string) *DemoCart { return NewCart(userID, Sku, time.Minute*60, cartType) } func NewCartScenes02(userID string, Sku int64, TTL time.Duration) *DemoCart { return NewCart(userID, Sku, TTL, "") }
上面的代碼看起來沒什麼問題,可是咱們設計代碼最重要的考慮就是穩定與變化,咱們須要作到 對擴展開放,對修改關閉 以及代碼的 高內聚。那麼若是是上面的代碼,你在 CartExts
增長了一個字段或者減小了一個字段。是否是每一個地方都須要進行修改呢?又或者 CartExts
若是有很是多的字段,這個不一樣場景的構造函數是否是得寫很是多個?因此簡要概述一下上面的辦法存在的問題。函數
CartExts
字段進行擴展;CartExts
字段很是多,構造函數參數很長,難看、難維護;NewCart
中,麪條代碼不優雅;CartExts
作爲參數的方式,那麼就將過多的細節暴露給了調用者。接下來咱們來看看 GRPC
是怎麼作的,學習優秀的範例,提高自個人代碼能力。學習
從這你也能夠體會到代碼功底牛逼的人,代碼就是寫的美!
源碼來自:grpc@v1.28.1 版本。爲了突出主要目標,對代碼進行了必要的刪減。
// dialOptions 詳細定義在 google.golang.org/grpc/dialoptions.go type dialOptions struct { // ... ... insecure bool timeout time.Duration // ... ... } // ClientConn 詳細定義在 google.golang.org/grpc/clientconn.go type ClientConn struct { // ... ... authority string dopts dialOptions // 這是咱們關注的重點,全部可選項字段都在這裏 csMgr *connectivityStateManager // ... ... } // 建立一個 grpc 連接 func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) { cc := &ClientConn{ target: target, csMgr: &connectivityStateManager{}, conns: make(map[*addrConn]struct{}), dopts: defaultDialOptions(), // 默認值選項 blockingpicker: newPickerWrapper(), czData: new(channelzData), firstResolveEvent: grpcsync.NewEvent(), } // ... ... // 修改改選爲用戶的默認值 for _, opt := range opts { opt.apply(&cc.dopts) } // ... ... }
上面代碼的含義很是明確,能夠認爲 DialContext
函數是一個 grpc 連接的建立函數,它內部主要是構建 ClientConn
這個結構體,並作爲返回值。defaultDialOptions
函數返回的是系統提供給 dopts 字段的默認值,若是用戶想要自定義可選屬性,能夠經過可變參數 opts 來控制。優化
通過上面的改進,咱們驚奇的發現,這個構造函數很是的優美,不管 dopts 字段如何增減,構造函數不須要改動;defaultDialOptions
也能夠從一個公有字段變爲一個私有字段,更加對內聚,對調用者友好。google
那麼這一切是怎麼實現的?下面咱們一塊兒學習這個實現思路。設計
首先,這裏的第一個技術點是,DialOption
這個參數類型。咱們經過可選參數方式優化了可選項字段修改時就要增長構造函數參數的尷尬,可是要作到這一點就須要確保可選字段的類型一致,實際工做中這是不可能的。因此又使出了程序界最高手段,一層實現不了,就加一層。代理
經過這個接口類型,實現了對各個不一樣字段類型的統一,讓構造函數入參簡化。來看一下這個接口。
type DialOption interface { apply(*dialOptions) }
這個接口有一個方法,其參數是 *dialOptions
類型,咱們經過上面 for 循環處的代碼也能夠看到,傳入的是 &cc.dopts
。簡單說就是把要修改的對象傳入進來。apply
方法內部實現了具體的修改邏輯。
那麼,這既然是一個接口,必然有具體的實現。來看一下實現。
// 空實現,什麼也不作 type EmptyDialOption struct{} func (EmptyDialOption) apply(*dialOptions) {} // 用到最多的地方,重點講 type funcDialOption struct { f func(*dialOptions) } func (fdo *funcDialOption) apply(do *dialOptions) { fdo.f(do) } func newFuncDialOption(f func(*dialOptions)) *funcDialOption { return &funcDialOption{ f: f, } }
咱們重點說 funcDialOption
這個實現。這算是一個高級用法,體現了在 Golang 裏邊函數是 一等公民。它有一個構造函數,以及實現了 DialOption
接口。
newFuncDialOption
構造函數接收一個函數作爲惟一參數,而後把傳入的函數保存到 funcDialOption
的字段 f
上。再來看看這個參數函數的參數類型是 *dialOptions
,與 apply
方法的參數是一致的,這是設計的第二個重點。
如今該看 apply
方法的實現了。它很是簡單,其實就是調用構造 funcDialOption
時傳入的方法。能夠理解爲至關於作了一個代理。把 apply
要修改的對象丟到 f
這個方法中。因此重要的邏輯都是咱們傳入到 newFuncDialOption
這個函數的參數方法實現的。
如今來看看 grpc 內部有哪些地方調用了 newFuncDialOption
這個構造方法。
因爲 newFuncDialOption
返回的 *funcDialOption
實現了 DialOption
接口,所以關注哪些地方調用了它,就能夠順藤摸瓜的找到咱們最初 grpc.DialContext
構造函數 opts 能夠傳入的參數。
調用了該方法的地方很是多,咱們只關注文章中列出的兩個字段對應的方法:insecure
與timeout
。
// 如下方法詳細定義在 google.golang.org/grpc/dialoptions.go // 開啓不安全傳輸 func WithInsecure() DialOption { return newFuncDialOption(func(o *dialOptions) { o.insecure = true }) } // 設置 timeout func WithTimeout(d time.Duration) DialOption { return newFuncDialOption(func(o *dialOptions) { o.timeout = d }) }
來體驗一下這裏的精妙設計:
DialOption
,從而確保了 grpc.DialContext
方法可用可選參數,由於類型都是一致的;*funcDialOption
,可是它實現了接口 DialOption
,這增長了擴展性。完成了上面的程序構建,如今咱們來站在使用的角度,感覺一下這無限的風情。
opts := []grpc.DialOption{ grpc.WithTimeout(1000), grpc.WithInsecure(), } conn, err := grpc.DialContext(context.Background(), target, opts...) // ... ...
固然這裏要介紹的重點就是 opts
這個 slice ,它的元素就是實現了 DialOption
接口的對象。而上面的兩個方法通過包裝後都是 *funcDialOption
對象,它實現了 DialOption
接口,所以這些函數調用後的返回值就是這個 slice 的元素。
如今咱們能夠進入到 grpc.DialContext
這個方法內部,看到它內部是如何調用的。遍歷 opts,而後依次調用 apply
方法完成設置。
// 修改改選爲用戶的默認值 for _, opt := range opts { opt.apply(&cc.dopts) }
通過這樣一層層的包裝,雖然增長了很多代碼量,可是明顯可以感覺到整個代碼的美感、可擴展性都獲得了改善。接下來看一下,咱們本身的 demo 要如何來改善呢?
首先咱們須要對結構體進行改造,將 CartExts
變成 cartExts
, 而且須要設計一個封裝類型來包裹全部的擴展字段,並將這個封裝類型作爲構造函數的可選參數。
const ( CommonCart = "common" BuyNowCart = "buyNow" ) type cartExts struct { CartType string TTL time.Duration } type CartExt interface { apply(*cartExts) } // 這裏新增了類型,標記這個函數。相關技巧後面介紹 type tempFunc func(*cartExts) // 實現 CartExt 接口 type funcCartExt struct { f tempFunc } // 實現的接口 func (fdo *funcCartExt) apply(e *cartExts) { fdo.f(e) } func newFuncCartExt(f tempFunc) *funcCartExt { return &funcCartExt{f: f} } type DemoCart struct { UserID string ItemID string Sku int64 Ext cartExts } var DefaultExt = cartExts{ CartType: CommonCart, // 默認是普通購物車類型 TTL: time.Minute * 60, // 默認 60min 過時 } func NewCart(userID string, Sku int64, exts ...CartExt) *DemoCart { c := &DemoCart{ UserID: userID, Sku: Sku, Ext: DefaultExt, // 設置默認值 } // 遍歷進行設置 for _, ext := range exts { ext.apply(&c.Ext) } return c }
通過這一番折騰,咱們的代碼看起來是否是很是像 grpc 的代碼了?還差最後一步,須要對 cartExts
的每一個字段包裝一個函數。
func WithCartType(cartType string) CartExt { return newFuncCartExt(func(exts *cartExts) { exts.CartType = cartType }) } func WithTTL(d time.Duration) CartExt { return newFuncCartExt(func(exts *cartExts) { exts.TTL = d }) }
對於使用者來講,只需以下處理:
exts := []CartExt{ WithCartType(CommonCart), WithTTL(1000), } NewCart("dayu", 888, exts...)
是否是很是簡單?咱們再一塊兒來總結一下這裏代碼的構建技巧:
2
中的接口類型;(這一步並不是必須,但這是一種良好的編程風格)按照上面的五步大法,你就可以實現設置默認值的高階玩法。
若是你喜歡這個類型的文章,歡迎留言點贊!