YAMAGUCHI::weblog

噛み付き地蔵に憧れて、この神の世界にやってきました。マドンナみたいな男の子、コッペです。

or-done-channelでコードの可読性を上げる

はじめに

こんにちは、キーボード自作おじさんです。このエントリはGo Advent Calendar 2017の4日目の記事です。 今年のエントリーは大作が並ぶアドベントカレンダーの休憩用のエントリーと思っていただければ幸いです。

Goの並列処理のパターン

Goが公開されてからもう8年になり、Goが得意とする並列処理にもGo特有のパターンなどがコミュニティ内で蓄積されてきました。 その中でもよく聞くものとしては

  • for-select loop
  • or-channel
  • or-done-channel
  • tee-channel
  • fan-in, fan-out

などがあります。今日はその中でも or-done-channel について書こうと思います。

どういうときに or-done-channel を使うか

上限の数が決まっているような処理を行う場合に

  • データのソースからの入力が終わってしまった場合(channelのcloseに対応)
  • 上限の数に達してしまった場合(doneに対応)

のいずれかに当てはまる場合に便利なパターンです。

rangeを使う場合

まずchannelがcloseするまでの処理を行う場合は、range キーワード使って forループを回すのが便利で可読性も高いです。

for v := range ch {
    something(v)
}

しかし、この場合は処理の上限に達してしまった場合が対応できません。何らかの形で ch と done の両方を受けてあげる必要があります。

for-select loopだけを使う場合

そうなるとGoで複数のchannelを使った並行処理を書こうと思った場合には for-select loop が一番良く出てくるので、それを書きたくなります。

LOOP:
for {
    select {
    case v, ok := <-ch:
        if !ok {
            break LOOP
        }
        something(v)
    case <-done:
        break LOOP
    }
}

これでとりあえず希望の処理は書けるのですが、どうも見た目が大きくなってしまいます。そこでこの処理を別途まとめて、rangeの書き方に持ち込むのが or-done-pattern です。

or-done-channel

deferの性質を使って done が来ても ch からのデータ取得が終わった場合にも、かならず stream を close しています。これで得られる stream が閉じられたどうかだけ意識すればよくなるため、普通に range のパターンに持ち込めるわけです。

func orDone(done <-chan bool, ch <-chan Data) <-chan Data {
    stream := make(chan Data)
    go func() {
        defer close(stream)
        for {
            select {
            case <-done:
                return
            case v, ok := <-ch:
                if !ok {
                    return
                }
                stream <- v
            }
        }
    }()
    return stream
}

for v := range orDone(done, ch) {
    something(v)
}

Goはシンプルな文法ながら、 goroutinechannelfordefer などの機能をうまく組み合わせると非常に柔軟な書き方ができるので、こういった書き方との出会いがたくさんあって日々勉強になっています。皆さんも面白いパターンを知ったらぜひ教えてください。

明日は @cia_rana さんです。