八、GO程式設計模式:PIPELINE

zhaocrazy發表於2022-02-08

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

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
    }

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

八、GO程式設計模式:PIPELINE

延伸閱讀

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

(全文完)本文非本人所作,轉載左耳朵耗子部落格和出處 酷 殼 – CoolShell

本作品採用《CC 協議》,轉載必須註明作者和本文連結
滴水穿石,石破天驚----馬乂

相關文章