🤖

🤖

:gijutsu_burogu:

Goでのストリームの使い方と使うべき理由

はじめに

Go ならわかるシステムプログラミングを読みました。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

  • 作者:渋川 よしき
  • 発売日: 2017/10/23
  • メディア: 単行本(ソフトカバー)

僕は本で読みましたが、元々は Web で連載していたみたいで無料で読むことができます。(本の方がコンテンツは充実しています)

ascii.jp

Go でのストリームを紹介します。 厳密には、Go ではストリームという言い方はしますせんが、io.Readerio.Writerでストリームのようなデータが流れるパイプを実現できます。

ストリームとは

ストリーム(英: stream)とは、連続したデータを「流れるもの」として捉え、そのデータの入出力あるいは送受信を扱うことであり、またその操作のための抽象データ型を指す[1]。出力ストリーム (output stream) を利用してデータの書き込みを行ない、また入力ストリーム (input stream) を利用してデータの読み出しを行なう。ファイルの入出力を扱うもの、メモリバッファの入出力を扱うもの、ネットワーク通信を扱うものなどさまざまなものがある。ref:wikipedia

この説明では分かりにくいですが、データの流れを扱うための抽象化された仕組みのことを表していて、これによってデータの流れを簡潔に書くことができるというイメージです。

例えば、Java でのストリーム

// widgetsというデータがある
int sum = widgets.stream()
                 // フィルタリングにより、赤widgetsのみを含むストリームへ変換
                 .filter(w -> w.getColor() == RED)
                 // 各赤widgetsの重量を表すint値のストリームへ変換
                 .mapToInt(w -> w.getWeight())
                 // 合計重量を計算
                 .sum();

Go でのストリーム

io.MultiReader,io.TeeReader,io.LimitReader,io.SectionReader,io.Pipeを使うことでデータの流れを制御することができます。

package main

import (
    "bytes"
    "io"
    "os"
    "strings"
)

func main() {
    docker := strings.NewReader("docker")
    teams := strings.NewReader("teams")
    zero := strings.NewReader("0")

    // offsetとsizeを指定する
    k := io.NewSectionReader(docker, 3, 1)
    o := io.NewSectionReader(docker, 1, 1)
    // 先頭からのsizeを指定する
    t := io.LimitReader(teams, 1)
    a := io.NewSectionReader(teams, 2, 1)
    r := io.NewSectionReader(docker, 5, 1)

    // oをooooにするパイプ
    // Multiwriterにより実現する
    forOOOO := io.NewSectionReader(docker, 1, 1)
    pr, pw := io.Pipe()
    writer := io.MultiWriter(pw, pw, pw, pw)
    go io.Copy(writer, forOOOO)
    defer pw.Close()

    // 出力する
    stream := io.MultiReader(k, o, t, a, r, io.LimitReader(pr, 4), zero)
    io.Copy(os.Stdout, stream)
}
// Output:
// kotaroooo0

Go のPipeでは、Read()が先に呼ばれると誰かが Write()を呼ぶまでブロックし、Write()が先に呼ばれると誰かが Read()を呼ぶまでブロックします。 したがって、ゴルーチンによる並列化によりそれらを非同期で共存するようにすること(同時に呼ぶイメージ)でデッドロックを回避します。

qiita.com

なぜストリームで扱うのか

メモリ効率

[]byteに変換するとメモリ消費が大きいです。 io.Readerは同じバイトを使い回すのでメモリ効率が良いです。

上例のkotaroooo0の文字列処理程度ではメンテナンス性とメモリ効率を天秤にかけると、ストリームで処理すべきではないです。 しかし、サイズの大きいテキストや JSON を扱うときはストリームは有効です。

標準パッケージで io.Reader を多く実装

jsonos.File,jpeg,pngなど多くの標準パッケージでio.Readerが実装されており、使い所が多いです。

Go での実用的な例

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    // BAD
    var u User
    data, _ := ioutil.ReadAll(r.Body) // メモリへ展開してしまっており、サイズが大きいリクエストボディの場合メモリ効率が悪い
    err = json.Unmarshal(data, &u)
    // ...

    // GOOD
    var u User
    err = json.NewDecoder(r.Body).Decode(&u)
    // ...
    })

問題

ASCII.jp:Goならわかるシステムプログラミング より引用します。 連載を読みながらでないと隠れた文脈を理解できないので、実際に解くのはハードルが高いです。 解答を見て、雰囲気を理解するだけでも良いと思います。

これらの文字列を 3 つの入力ストリーム(io.Reader)とし、次に示す main() 関数のコメント部にコードを追加して、最後の io.Copy で「ASCII」という文字列が出力されるようにしてみてください。

package main

import (
    "strings"
    "io"
    "os"
)

var (
    computer    = strings.NewReader("COMPUTER")
    system      = strings.NewReader("SYSTEM")
    programming = strings.NewReader("PROGRAMMING")
)

func main() {
    var stream io.Reader

    // ここにioパッケージ何か書く
    // 使っていいのはioパッケージの内容+基本文法のみです
    // io.Pipe を使う場合はブロッキングを防ぐためにゴルーチンを使ってください

    io.Copy(os.Stdout, stream)
}

答え

kotaroooo0の文字列をストリームで生成した時と同様にできます。

    a := io.NewSectionReader(programming, 5, 1)
    s := io.LimitReader(system, 1)
    c := io.LimitReader(computer, 1)
    i := io.NewSectionReader(programming, 8, 1)
    pr, pw := io.Pipe()
    writer := io.MultiWriter(pw, pw)
    go io.CopyN(writer, i, 1)
    defer pw.Close()
    stream = io.MultiReader(a, s, c, io.LimitReader(pr, 2))

    io.Copy(os.Stdout, stream)

参考

ascii.jp

christina04.hatenablog.com

yosuke-furukawa.hatenablog.com