Go編程模式:Pipeline

本篇文章,我們着重介紹Go編程中的Pipeline模式。對於Pipeline用過Unix/Linux命令行的人都不會陌生,他是一種把各種命令拼接起來完成一個更強功能的技術方法。在今天,流式處理,函數式編程,以及應用網關對微服務進行簡單的API編排,其實都是受pipeline這種技術方式的影響,Pipeline這種技術在可以很容易的把代碼按單一職責的原則拆分成多個高內聚低耦合的小模塊,然後可以很方便地拼裝起來去完成比較複雜的功能。

本文是全系列中第8 / 9篇:Go編程模式

« 上一篇文章 下一篇文章 »

HTTP 處理

這種Pipeline的模式,我們在《Go編程模式:修飾器》中有過一個示例,我們在這裏再重溫一下。在那篇文章中,我們有一堆如 WithServerHead()WithBasicAuth()WithDebugLog()這樣的小功能代碼,在我們需要實現某個HTTP API 的時候,我們就可以很容易的組織起來。

原來的代碼是下面這個樣子:

http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))

通過一個代理函數:

type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc
func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}

我們就可以移除不斷的嵌套像下面這樣使用了:

http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))

Channel 管理

當然,如果你要寫出一個泛型的pipeline框架並不容易,而使用Go Generation,但是,我們別忘了Go語言最具特色的 Go Routine 和 Channel 這兩個神器完全也可以被我們用來構造這種編程。

Rob Pike在 Go Concurrency Patterns: Pipelines and cancellation 這篇blog中介紹瞭如下的一種編程模式。

Channel轉發函數

首先,我們需一個 echo()函數,其會把一個整型數組放到一個Channel中,並返回這個Channel

func echo(nums []int) <-chan int {
  out := make(chan int)
  go func() {
    for _, n := range nums {
      out <- n
    }
    close(out)
  }()
  return out
}

然後,我們依照這個模式,我們可以寫下這個函數。

平方函數
func sq(in <-chan int) <-chan int {
  out := make(chan int)
  go func() {
    for n := range in {
      out <- n * n
    }
    close(out)
  }()
  return out
}
過濾奇數函數
func odd(in <-chan int) <-chan int {
  out := make(chan int)
  go func() {
    for n := range in {
      if n%2 != 0 {
        out <- n
      }
    }
    close(out)
  }()
  return out
}
求和函數
func sum(in <-chan int) <-chan int {
  out := make(chan int)
  go func() {
    var sum = 0
    for n := range in {
      sum += n
    }
    out <- sum
    close(out)
  }()
  return out
}

然後,我們的用戶端的代碼如下所示:(注:你可能會覺得,sum()odd()sq()太過於相似。你其實可以通過我們之前的Map/Reduce編程模式或是Go Generation的方式來合併一下

var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range sum(sq(odd(echo(nums)))) {
  fmt.Println(n)
}

上面的代碼類似於我們執行了Unix/Linux命令: echo $nums | sq | sum

同樣,如果你不想有那麼多的函數嵌套,你可以使用一個代理函數來完成。

type EchoFunc func ([]int) (<- chan int) 
type PipeFunc func (<- chan int) (<- chan int) 

func pipeline(nums []int, echo EchoFunc, pipeFns ... PipeFunc) <- chan int {
  ch  := echo(nums)
  for i := range pipeFns {
    ch = pipeFns[i](ch)
  }
  return ch
}

然後,就可以這樣做了:

var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}    
for n := range pipeline(nums, gen, odd, sq, sum) {
    fmt.Println(n)
  }

Fan in/Out

動用Go語言的 Go Routine和 Channel還有一個好處,就是可以寫出1對多,或多對1的pipeline,也就是Fan In/ Fan Out。下面,我們來看一個Fan in的示例:

我們想通過併發的方式來對一個很長的數組中的質數進行求和運算,我們想先把數組分段求和,然後再把其集中起來。

下面是我們的主函數:

func makeRange(min, max int) []int {
  a := make([]int, max-min+1)
  for i := range a {
    a[i] = min + i
  }
  return a
}

func main() {
  nums := makeRange(1, 10000)
  in := echo(nums)

  const nProcess = 5
  var chans [nProcess]<-chan int
  for i := range chans {
    chans[i] = sum(prime(in))
  }

  for n := range sum(merge(chans[:])) {
    fmt.Println(n)
  }
}

再看我們的 prime() 函數的實現 :

func is_prime(value int) bool {
  for i := 2; i <= int(math.Floor(float64(value) / 2)); i++ {
    if value%i == 0 {
      return false
    }
  }
  return value > 1
}

func prime(in <-chan int) <-chan int {
  out := make(chan int)
  go func ()  {
    for n := range in {
      if is_prime(n) {
        out <- n
      }
    }
    close(out)
  }()
  return out
}

我們可以看到,

  • 我們先製造了從1到10000的一個數組,
  • 然後,把這堆數組全部 echo到一個channel裏 – in
  • 此時,生成 5 個 Channel,然後都調用 sum(prime(in)) ,於是每個Sum的Go Routine都會開始計算和
  • 最後再把所有的結果再求和拼起來,得到最終的結果。

其中的merge代碼如下:

func merge(cs []<-chan int) <-chan int {
  var wg sync.WaitGroup
  out := make(chan int)

  wg.Add(len(cs))
  for _, c := range cs {
    go func(c <-chan int) {
      for n := range c {
        out <- n
      }
      wg.Done()
    }(c)
  }
  go func() {
    wg.Wait()
    close(out)
  }()
  return out
}

用圖片表示一下,整個程序的結構如下所示:

延伸閱讀

如果你還想了解更多的這樣的與併發相關的技術,可以參看下面這些資源:

(全文完)


關注CoolShell微信公衆賬號和微信小程序

(轉載本站文章請註明作者和出處 酷 殼 – CoolShell ,請勿用於任何商業用途)

——=== 訪問 酷殼404頁面 尋找遺失兒童。 ===——
好爛啊 有點差 湊合看看 還不錯 很精彩 (沒人打分)

Loading...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章