Go設計模式學習筆記

學習對象:https://github.com/tmrts/go-p...。這個repo使用go語言實現了一些設計模式,包括經常使用的Builder模式,Singleton模式等,也有列舉出還未用go實現的模式,如Bridge模式等。java

本文並不是完整地介紹和解析這個repo裏的每一行代碼,只對我的認爲值得學習和記錄的地方進行說明,閱讀過repo代碼後再閱讀本文比較合適。git

Functional Options

這個模式是一種優雅地設置對象初始化參數的方式。考慮的點是:github

  • 如何友好地擴展初始化的選填參數
  • 如何友好地處理默認值問題
  • 函數簽名見名知意

比較如下幾種初始化對象參數的方法:設計模式

//name是必填參數, timeout和maxConn是選填參數,若是不填則設置爲默認值

// pattern #1
func NewServer(name string, timeout time.Duration, maxConn uint) (*Server, error) {...}
// 這種方法最直觀, 但也是最不合適的, 由於對於擴展參數須要修改函數簽名, 且默認值須要經過文檔獲知

// pattern #2
type ServerConf struct {
    Timeout time.Duration
    MaxConn uint
}
func NewServer(name string, conf ServerConf) (*Server, error) {...} // 1)
func NewServer(name string, conf *ServerConf) (*Server, error) {...} // 2)
func NewServer(name string, conf ...ServerConf) (*Server, error) {...} // 3)
// 改進: 使用了參數結構體, 增長參數不須要修改函數簽名
// 1) conf如今是必傳, 實際上裏面的是選填參數
// 2) 避免nil; conf可能在外部被改變.
// 3) 都使用默認值的時候能夠不傳, 但多個conf可能在配置上有衝突
// conf的默認空值對於Server多是有意義的.

// pattern #3: Functional Options
type ConfSetter func(srv *Server) error
func ServerTimeoutSetter(t time.Duration) ConfSetter {
    return func(srv *Server) error {
        srv.timeout = t
        return nil        
    }
}
func ServerMaxConnSetter(m uint) ConfSetter {
    return func(srv *Server) error {
        srv.maxConn = m
        return nil
    }
}
func NewServer(name string, setter ...ConfSetter) (*Server, error) {
    srv := new(Server)
    ...
    for _, s := range setter {
        err := s(srv)
    }
    ...
}
// srv, err := NewServer("name", ServerTimeoutSetter(time.Second))
// 使用閉包做爲配置參數. 若是不須要配置選填參數, 只須要填參數name.

上面的pattern#2嘗試了三種方法來優化初始化參數的問題,但每種方法都有本身的不足之處。pattern#3,也就是Functional Options,經過使用閉包來作優化,從使用者的角度來看,已是足夠簡潔和明確了。固然,代價是初次理解這種寫法有點繞,不如前兩種寫法來得直白。trade offapi

欲言又止稍加思考,容易提出這個問題:這跟Builder模式有什麼區別呢?我的認爲,Functional Options模式本質上就是Builder模式:經過函數來設置參數。閉包

參考文章:Functional options for friendly APIs併發

Circuit-Breaker

熔斷模式:若是服務在一段時間內不可用,這時候服務要考慮主動拒絕請求(減輕服務方壓力和請求方的資源佔用)。等待一段時間後(嘗試等待服務變爲可用),服務嘗試接收部分請求(一會兒涌入過多請求可能致使服務再次不可用),若是請求都成功了,再正常接收全部請求。函數

// 極其精簡的版本, repo中版本詳盡一些
type Circuit func() error
// Counter 的實現應該是一個狀態機
type Counter interface {
    OverFailureThreshold()
    UpdateFailure()
    UpdateSuccess()
}

var cnt Counter
func Breaker(c Circuit) Circuit {
    return func() {
        if cnt.OverFailureThreshold() {
            return fmt.Errorf("主動拒絕")
        }
        if err := c(); err != nil {
            cnt.UpdateFailure()
            return err
        }
        cnt.UpdateSuccess()
        return nil
    }
}

熔斷模式更像是中間件而不是設計模式:熔斷器是一個抽象的概念而不是具體的代碼實現;另外,若是要實現一個實際可用的熔斷器,要考慮的方面仍是比較多的。舉些例子:須要提供手動配置熔斷器的接口,避免出現不可控的請求狀況;什麼類型的錯誤熔斷器才生效(惡意發送大量無效的請求可能致使熔斷器生效),等等。性能

參考文章:Circuit Breaker pattern
參考實現:gobreaker學習

Semaphore

go的標準庫中沒有實現信號量,repo實現了一個:)
repo實現的實質是使用chan。chan自己已經具有互斥訪問的功能,並且能夠設定緩衝大小,只要稍加修改就能夠看成信號量使用。另外,利用select語法,能夠很方便地實現超時的功能。

type Semaphore struct {
    resource chan struct{}   // 編譯器會優化struct{}類型, 使得全部struct{}變量都指向同一個內存地址
    timeout  time.Duration   // 用於避免長時間的死鎖
}
type TimeoutError error
func (s *Semaphore) Aquire() TimeoutError {
    select {
        // 會從上到下檢查是否阻塞
        // 若是timeout爲0, 且暫時不能得到/解鎖資源, 會當即返回超時錯誤
        case: <-s.resource:
            return nil
        case: <- time.After(s.timeout):
            return fmt.Errorf("timeout")
    } 
}
func (s *Semaphore) Release() TimeoutError {
    select {
        // 同Aquire()
        case: s.resource <- struct{}{}:
            return nil
        case: <- time.After(s.timeout):
            return fmt.Errorf("timeout")
    }   
}
func NewSemaphore(num uint, timeout time.Duration) (*Semaphore, error) {
    if num == 0 {
        return fmt.Errorf("invalid num")    //若是是0, 須要先Release才能Aquire.
    }
    return &Semaphore{
        resource: make(chan strcut{}, num),
        timeout:  timeout,
    }, nil    //其實返回值類型也不影響Semaphore正常工做, 由於chan是引用類型
}

Object Pool

標準庫的sync包已經有實現了一個對象池,可是這個對象池接收的類型是 interface{} (萬惡的範型),並且池裏的對象若是不被其它內存引用,會被gc回收(同java中弱引用的collection類型相似)。
repo實現的對象池是明確類型的(萬惡的範型+1),並且閒置不會被gc回收。但僅僅做爲展現說明,repo的實現沒有作超時處理。下面的代碼嘗試加上超時處理。也許對使用者來講,額外增長處理超時錯誤的代碼比較繁瑣,但這是有必要的,除非使用者通讀並理解了你的代碼。trade off

type Pool struct {
    pool     chan *Object
    timeout  time.Duration
}
type TimeoutError error
func NewPool(total int, timeout time.Duration) *Pool {
    p := &Pool {
        pool:     make(Pool, total),
        timeout:  timeout,
    }    //pool是引用類型, 因此返回類型能夠不是指針
    for i := 0; i < total; i++ {
        p.pool <- new(Object)
    }
    return p
}
func (p *Pool) Aquire() (*Object, TimeoutError) {
    select {
        case obj <- p.pool:
            return obj, nil
        case <- time.After(timeout):
            return nil, fmt.Errorf("timeout")
    }
}
func (p *Pool) Release(obj *Object) TimeoutError {
    select {
        case p.pool <- obj:
            return  nil
        case <- time.After(timeout):
            return nil, fmt.Errorf("timeout")
    }
}

chan and goroutine

解析一下repo裏goroutine和chan的使用方式,也不算是設計模式。

Fan-in pattern 主要體現如何使用sync.WaitGroup同步多個goroutine。思考:這裏的實現是若是cs的長度爲n, 那個要開n個goroutine, 有沒有辦法優化爲開常數個goroutine?

// 將若干個chan的內容合併到一個chan當中
func Merge(cs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(cs))
    // 將send函數在for循環中寫成一個不帶參數的匿名函數, 看起來會使代碼更簡潔,
    // 但實際上全部for循環裏的全部goroutine會公用一個c, 代碼不能正確實現功能.
    send := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }
    for _, c := range cs {
        go send(c)
    }    
    // 開一個goroutine等待wg, 而後關閉merge的chan, 不阻塞Merge函數
    go func() {
        wg.Wait()
        close(out)
    }
    return out
}

Fan-out pattern 將一個主chan的元素循環分發給若干個子chan(分流)。思路比較簡單就不貼代碼了。思考:reop實現的代碼,若是其中一個子chan沒有消費元素,那麼整個分發流程都會卡住。是否能夠優化?

Bounded Parallelism Pattern 比較完整的例子來講明如何使用goroutine. 面的例子是併發計算目錄下文件的md5.

func MD5All(root string) (map[string][md5.Size]byte, error) {    //由於byte是定長的, 使用數據更合適, 可讀且性能也好一點

    done := make(chan struct{})       //用於控制整個流程是否暫停. 其實這裏是用context可能會更好.
    defer close(done)

    paths, errc := walkFiles(done, root)

    c := make(chan result)
    var wg sync.WaitGroup
    const numDigesters = 20
    wg.Add(numDigesters)
    for i := 0; i < numDigesters; i++ {
        go func() {
            digester(done, paths, c) 
            wg.Done()
        }()
    }

    // 同上, 開goroutine等待全部digester結束
    go func() {
        wg.Wait()
        close(c) 
    }()

    m := make(map[string][md5.Size]byte)
    for r := range c {
        if r.err != nil {
            return nil, r.err
        }
        m[r.path] = r.sum
    }
    // 必須放在m處理結束後才檢查errc. 不然, 要等待walkFiles結束了才能開始處理m
    // 相反, 若是errc有信號, c確定已經close了
    if err := <-errc; err != nil {
        return nil, err
    }
    return m, nil
}

func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
    paths := make(chan string)  // 這裏能夠適當增長緩衝, 取決於walkFiles快仍是md5.Sum快
    errc := make(chan error, 1) //必須有緩衝, 不然死鎖. 上面的代碼paths close了才檢查errc
    go func() { 
        defer close(paths) // 這裏的defer沒必要要. defer是運行時的, 有成本.
        errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.Mode().IsRegular() {
                return nil
            }
            select {
            case paths <- path:
            case <-done:
                return errors.New("walk canceled")
            }
            return nil
        })
    }()
    return paths, errc
}

type result struct {
    path string
    sum  [md5.Size]byte
    err  error
}


func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
    for path := range paths {
        data, err := ioutil.ReadFile(path)
        select {
        // 看md5.Sum先結束仍是done信號先到來
        case c <- result{path, md5.Sum(data), err}:
        case <-done:
            return
        }
    }
}
相關文章
相關標籤/搜索