はじめに
Go ならわかるシステムプログラミングを読みました。
- 作者:渋川 よしき
- 発売日: 2017/10/23
- メディア: 単行本(ソフトカバー)
僕は本で読みましたが、元々は Web で連載していたみたいで無料で読むことができます。(本の方がコンテンツは充実しています)
Go でのストリームを紹介します。
厳密には、Go ではストリームという言い方はしますせんが、io.Reader
とio.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()
を呼ぶまでブロックします。
したがって、ゴルーチンによる並列化によりそれらを非同期で共存するようにすること(同時に呼ぶイメージ)でデッドロックを回避します。
なぜストリームで扱うのか
メモリ効率
[]byte
に変換するとメモリ消費が大きいです。
io.Reader
は同じバイトを使い回すのでメモリ効率が良いです。
上例のkotaroooo0
の文字列処理程度ではメンテナンス性とメモリ効率を天秤にかけると、ストリームで処理すべきではないです。
しかし、サイズの大きいテキストや JSON を扱うときはストリームは有効です。
標準パッケージで io.Reader を多く実装
json
やos.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)