在go的項目中,你們編碼的時候應該或多或少都看過go的一些源碼或者其餘開源項目的源碼,不知道你們有沒有感受本身寫出來的代碼相對於源碼有必定的差距,無論從結構定義,接口封裝等方面總感受差那麼點意思。反正我一直感受本身的編碼相對於go的源碼差距較大。既然有這麼好的代碼在面前,咱們如何根據這些源碼提取一些供本身學習的內容呢,此次準備根據源碼好好學習一下,簡單總結以下,若是後續還有心得會逐步更新。git
最近正好用到的兩個開源代碼:
一、github.com/olivere/elastic/v7
二、google.golang.org/grpcgithub
本身的項目調用這兩個包時,有以下的代碼(若是有興趣能夠本身去github上看完整的源碼)
一、用elastic包構建聚合查詢golang
agg := elastic.NewTermsAggregation(). Size(10000). Field("data.sce")
二、用grpc構造鏈接app
conn, err := grpc.DialContext(connCtx, "127.0.0.1:5555", grpc.WithInsecure(), grpc.WithBlock()) if err != nil { return err }
拋開兩個函數的功能不談,其實兩個函數均可以理解爲一種構造或者初始化函數,其Size,Field,WithInsecure,WithBlock等函數都是在初始化或者構造時提供某種參數而已。函數
那咱們提煉一下,就是當咱們構造一個對象時,通常會提供不少參數用來構造,可是不一樣場景,或者不一樣條件下,須要的參數又不一樣,如何來封裝這些構造函數來方便使用呢?學習
以以下結構爲例,假設是一個代理優化
type Agency struct { IP int32 //required Protocol string //optional Timeout time.Duration //optional }
直接構造不一樣的構造函數:ui
func NewAgency(ip int32) *Agency func NewAgencyWithProtocol(ip int32, p string) *Agency func NewAgencyWithProtocolAndTimeout(ip int32, p string, t time.Duration) *Agency ......
你會發現須要定義一系列的函數,並且隨着參數增多,可能擴充的函數也比較多,同時因爲go中不支持多態,每一個函數還要有不一樣的名稱,當你此次調用了A,下次增長參數時,還得改成調用B,可見麻煩多多,既很差看,也很差用,還很差維護。固然實際項目中可能也沒人會這麼寫,這裏只是舉例而已。google
直接將Agency結構體做爲參數,這樣參數不就固定了嗎,並且一舉解決全部問題,只用一個構造函數便可。編碼
func NewAgency(a Agency) *Agency
這種方法現實項目中確實也有使用的哦,那對於這種簡單的結構體,還算比較方便簡潔,可是若是結構體成員較多(像goroutine等結構),動輒20+以上,那你初始化的時候還得先一一肯定參數,而後再去調用構造函數是否是也麻煩,並且不少參數其實用不到賦值,只要默認值就夠用了。並且成員變量一多,你可能都不知道那些是必選參數,那些是可選參數了。
那如何解決這個問題呢?
修改Agency的結構體,將可選成員和必選成員分開:
type AgencyOption struct { Protocol string //optional Timeout time.Duration //optional TLS tls.Conn //optional } type Agency struct { IP int32 //required AgencyOption }
而後定義一個構造函數
func NewAgency(ip int32, param *AgencyOption) *Agency
這個函數區分了必選項和可選項,ip必填,param可選,若是param爲nil則不用對可選參數賦值。
這種方案相對於上一個方法略有改進,對於必選參數一目瞭然,可是對於參數較多的場景仍是沒有根本解決。
直接將可選參數不放在構造函數中,定義多個設置函數,例如:
func (a *Agency)SetProtocol(p string) { a.Protocol = p } func (a *Agency)SetTimeout(t time.Duration) { a.Timeout = t }
調用者使用方式:
a1 := NewAgency(ip) a1.SetProtocol("udp") a1.SetTimeout(100)
上述方法須要屢次調用,咱們作個修改:
func (a *Agency)SetProtocol(p string) *Agency{ a.Protocol = p return a } func (a *Agency)SetTimeout(t time.Duration) *Agency{ a.Timeout = t return a }
這樣,調用者可使用鏈式調用:
a1 := NewAgency(ip).SetTimeout(100).SetProtocol("udp")
這也是一種經常使用的方法。
像文章開篇提到的elastic庫就是這麼玩的,每次查詢的時候可能有不少參數要設置,直接連續調用便可,很清晰,這也是從這個開源庫中學到。之後就能夠直接應用到實際項目中了哦!
那若是就想將參數一把傳入,一次性初始化完成呢?
一次性傳入任意多個參數,首先咱們能夠想到go支持變參,例如func Add(base int, others ...int),能夠處理任意個數的int類型,可是咱們的參數通常是不同的,那咱們如何利用這種方案呢?
咱們大膽想象一下若是有這麼一種通用類型可使用(先不考慮返回值):
func NewAgency(ip int32, options ...Option)
若是能實現這樣一個構造函數,那麼可選參數的問題也就搞定了!!
可是這個通用的Option如何定義呢??使用某一種具體類型確定是不能完成的,那是否能夠將這個Option定義爲一個函數或者接口類型呢?
先從函數思考,看可否實現:
一個函數,無非是函數名,入參,邏輯處理,返回值這些東東
函數名,whatever,隨便起個能自注釋的便可;
入參,先放一下;
邏輯處理,就是這個函數要作啥,想一想咱們的最終目的就是將可選參數設置到咱們的對象中去!!那麼這個邏輯處理就相似於:
agency.Timeout = time 以及 agency.Protocol = "udp" 等等...
從邏輯處理看咱們要將參數設置到對象中,那這個對象是否是能夠做爲咱們的共同參數,那麼這個Option的定義是否是能夠爲:
type Option func(a *Agency)
由於是要改變Agency中的值,因此用的是指針做爲入參。
既然Option定義好了,那麼針對每一個參數咱們來實現由參數如何轉換爲這種Option傳入構造函數吧,即建立入參是參數,可是返回值是Option類型的函數
對於超時時間:
func Timeout(t time.Duration) Option{ return func(a *Agency){ a.Timeout = t } }
對於協議設置:
func Protocol(p string) Option{ return func(a *Agency){ a.Protocol = p } }
構造函數爲:
func NewAgency(ip int32, options ...Option) *Agency { a := &Agency{IP:ip} for _, opt := range options{ opt(a) } return a }
調用者爲:
a1 := NewAgency(ip) a2 := NewAgency(ip, Timeout(100)) a4 := NewAgency(ip, Timeout(200), Protocol("udp"))
這樣就清晰明瞭了吧,大功告成!
上個方法中定義的Option是一個函數類型,那麼接口類型是否也能夠勝任呢?
答案也是能夠的,這個是我從grpc的實現反向思考的過程,你們能夠參考,或者直接擼grpc的源碼看:)
咱們仍是以Agency爲例
首先定義這個Option接口,因爲在go中一般定義的接口名都帶er,咱們也遵守傳統定義爲:
type Optioner interface { apply() }
接口先只定義了一個名字,入參和返回值待定。
既然有了接口,那麼咱們就要定義一個類型來實現這個接口:
type RealOption struct { } func (ro *RealOption)apply(){ }
雛形就有了,那麼如何來填這些定義的內容呢?
先別急,咱們繼續定義設置參數的函數,返回值類型都要爲Optioner,因此其模型相似爲:
func SetProtocol(p string) Optioner { return &RealOption{ } } func SetTimeout(t time.Duration) Optioner { return &RealOption{ } }
再繼續看咱們最終可以提供的構造函數,應該是這個樣子:
func NewAgency(ip int32, options ...Optioner) *Agency { a := &Agency{IP:ip} for _, opt := range options{ opt.apply() } return a }
核心仍是遍歷可變參數options,去調用對應的接口設置相應的參數,從這裏做爲突破口,那麼apply這個接口類型應該定義成什麼呢,是否是呼之欲出了,只要增長個入參,無需返回值
apply(agency *Agency)
有了入參後,上述涉及參數的各個定義修改成:
func (ro *RealOption)apply(agency *Agency){ } type Optioner interface { apply(agency *Agency) } func NewAgency(ip int32, options ...Optioner) *Agency { a := &Agency{IP:ip} for _, opt := range options{ opt.apply(a) } return a }
既然接口定義好了,那麼看如何實現參數設置函數的邏輯,對於SetTimeout函數,目的是將入參t傳到對象中去,那麼就相似於:
func SetTimeout(t time.Duration) Optioner { return &RealOption{ ?:t, } }
若是?的位置是具體的類型,那麼這個t實際上是設置到了RealOption中,並無設置到Agency中,那麼怎麼辦呢,還記得上個方法中的Option定義嗎 ,其就是一個通用類型的函數,將參數設置到Agency裏,那麼這個地方也用這種方式呢,即:
func SetTimeout(t time.Duration) Optioner { return &RealOption{ ?:func(agency *Agency){ agency.Timeout = t }, } }
那這樣RealOption的定義也就出來了:
type RealOption struct { f func(agency *Agency) }
那麼全部的內容基本都完成了,總體代碼以下:
type Optioner interface { apply(agency *Agency) } type RealOption struct { f func(agency *Agency) } func (ro *RealOption) apply(agency *Agency) { ro.f(agency) } func SetProtocol(p string) Optioner { return &RealOption{ f: func(agency *Agency) { agency.Protocol = p }, } } func SetTimeout(t time.Duration) Optioner { return &RealOption{ f: func(agency *Agency) { agency.Timeout = t }, } } func NewAgency(ip int32, options ...Optioner) *Agency { a := &Agency{IP: ip} for _, opt := range options { opt.apply(a) } return a }
調用方式跟上一種方法同樣,不過傳入的可選參數是接口而已
繼續優化一下,對RealOption結構也提供一個構造函數,那麼參數設置函數改成:
func NewRealOption(f func(agency *Agency)) *RealOption { return &RealOption{ f: f, } } func SetProtocol(p string) Optioner { return NewRealOption(func(agency *Agency) { agency.Protocol = p }) } func SetTimeout(t time.Duration) Optioner { return NewRealOption(func(agency *Agency) { agency.Timeout = t }) }
OK,大功告成!這種方法就對應了開篇提到的grpc中實現的方法。
看似很簡單的一個功能,好的開源代碼總會採用各類方式,值得咱們在實際項目中多多體會,若是你們有什麼更好的方法歡迎留言交流分享。