如何把 Golang 的 channel 用的如 Node.js 的 stream 一樣絲滑

jeremy1127發表於2019-09-13

如果讓我和別人說說Golang有什麼特點,我首先想到不一定是goroutine,但一定會是channel。

因為Channel的存在,是讓Goroutine威力加成的利器。

如果用一句話來解釋channel的作用,我會說

Chanel是一個管道,它會讓資料流動起來。

++那麼如何理解這個讓資料流程起來呢?++

假如說你需要對100次請求,做兩種比較耗時的操作,然後再統計加權結果,還需要儘可能的併發來提高效能。示例程式碼如下:

var multipleChan = make(chan int, 4)
var minusChan = make(chan int, 4)
var harvestChan = make(chan int, 4)

defer close(multipleChan)
defer close(minusChan)
defer close(harvestChan)

go func() {
    for i:=1;i<=100;i++{
        multipleChan <- i
    }
}()

for i:=0; i<4; i++{
    go func() {
        for data := range multipleChan {
            minusChan <- data * 2
            time.Sleep(10* time.Millisecond)
        }
    }()

    go func() {
        for data := range minusChan {
            harvestChan <- data - 1
            time.Sleep(10* time.Millisecond)
        }
    }()
}

var sum = 0
var index = 0
for data := range harvestChan{
    sum += data
    index++
    if index == 100{
        break
    }
}

fmt.Println(sum)

不要笑這段程式碼簡單,如果考慮到錯誤處理的情況,那還是有些複雜的。比如,某個環節是遇到錯誤可以忽略,某個環節是遇到要終止所有操作;再加上,有時只關心第一個滿足條件的返回值,還需要超時處理。

寫一遍也許還可以,要是很多地方都要這樣寫,那真是頭大>_<!!!

重複的程式碼是萬惡之源,Don't repeat yourself是成為優秀工程師的第一步

於是,channelx這個庫誕生了!

使用了這個庫,實現上述同樣的功能,程式碼是這樣子的~~

var sum = 0

NewChannelStream(func(seedChan chan<- Result, quitChannel chan struct{}) {
    for i:=1; i<=100;i++{
        seedChan <- Result{Data:i}
    }
    close(seedChan) //記得關閉哦~~~
}).Pipe(func(result Result) Result {
    return Result{Data: result.Data.(int) * 2}
}).Pipe(func(result Result) Result {
    return Result{Data: result.Data.(int) - 1}
}).Harvest(func(result Result) {
    sum += result.Data.(int)
})

fmt.Println(sum)

我喜歡鏈式風格,所以寫成這個樣子,你也可以拆開來寫的。但重點是程式碼這樣寫起來是不是很絲滑,有nodejs stream流的快感呢,嘻嘻~~

除了Pipe->Harvest的組合,還可以實現Pipe->Race, Pipe->Drain, Pipe->Cancel等操作的組合。

這些複雜的例子,都可以參照stream_test.go檔案中的單元測試來實現,就不一一貼程式碼出來了哈。

那麼,這個stream又是如何實現的呢?核心就在NewChannelStreamPipe這個兩個函式里。

func NewChannelStream(seedFunc SeedFunc, optionFuncs ...OptionFunc) *ChannelStream {
    cs := &ChannelStream{
        workers:     runtime.NumCPU(),
        optionFuncs: optionFuncs,
    }

    for _, of := range optionFuncs {
        of(cs)
    }

    if cs.quitChan == nil {
        cs.quitChan = make(chan struct{})
    }

    cs.dataChannel = make(chan Item, cs.workers)

    go func() {
        inputChan := make(chan Item)

        go seedFunc(inputChan, cs.quitChan)

    loop:
        for {
            select {
            case <-cs.quitChan:
                break loop

            case res, ok := <-inputChan:
                if !ok {
                    break loop
                }

                select {
                case <-cs.quitChan:
                    break loop
                default:
                }

                if res.Err != nil {
                    cs.errors = append(cs.errors, res.Err)
                }

                if !cs.hasError && res.Err != nil {
                    cs.hasError = true
                    cs.dataChannel <- res
                    if cs.ape == stop {
                        cs.Cancel()
                    }
                    continue
                }

                if cs.hasError && cs.ape == stop {
                    continue
                }

                cs.dataChannel <- res
            }
        }

        safeCloseChannel(cs.dataChannel)

    }()

    return cs
}

func (p *ChannelStream) Pipe(dataPipeFunc PipeFunc, optionFuncs ...OptionFunc) *ChannelStream {
    seedFunc := func(dataPipeChannel chan<- Item, quitChannel chan struct{}) {
        wg := &sync.WaitGroup{}
        wg.Add(p.workers)
        for i := 0; i < p.workers; i++ {
            go func() {
                defer wg.Done()
            loop:
                for {
                    select {
                    case <-quitChannel:
                        break loop
                    case data, ok := <-p.dataChannel:
                        if !ok {
                            break loop
                        }

                        select {
                        case <-quitChannel:
                            break loop
                        default:
                        }

                        dataPipeChannel <- dataPipeFunc(data)
                    }
                }
            }()
        }

        go func() {
            wg.Wait()
            safeCloseChannel(dataPipeChannel)
        }()
    }

    mergeOptionFuncs := make([]OptionFunc, len(p.optionFuncs)+len(optionFuncs)+1)
    copy(mergeOptionFuncs[0:len(p.optionFuncs)], p.optionFuncs)
    copy(mergeOptionFuncs[len(p.optionFuncs):], optionFuncs)
    mergeOptionFuncs[len(p.optionFuncs)+len(optionFuncs)] = passByQuitChan(p.quitChan) //這行保證了整個stream中有一個唯一的quitChan

    return NewChannelStream(seedFunc, mergeOptionFuncs...)
}

程式碼看著多,刨除初始化的程式碼、錯誤處理和退出處理的程式碼,核心還是透過channel的資料流動。

首先,NewChannelStream中會新建一個inputChan傳入seedFunc,然後資料會透過seedChan(即inputChan),傳到dataChannel。

然後,當呼叫Pipe的時候,Pipe函式會自己建立一個seedFunc從上一個channelStream的dataChannel傳到dataPipeChannel中。這個Pipe中的seedFunc又會傳入NewChannelStream中,產生一個新channelStream物件,這時在新的channelStream中,inputChan即Pipe中的dataPipeChannel,整個資料流就這樣串起來了,過程如下:

inputChan(seedChan)->dataChannel->inputChan(dataPipeChannel)->dataChannel->....

分析過原始碼,再來看使用ChannelStream的例子和直接用Channel的例子,兩個dataChannel分別對應的是multipleChan和minusChan,多出的兩個inputChan,就是用這個庫額外的開銷嘍。

原創不易,你的支援就是對我最大的鼓勵,歡迎給channelx點個star!:)

未完待續,channelx中還會陸續增加各種常用場景的channel實現,敬請期待……

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章