📄

Goならわかるシステムプログラミング2版3章読書メモ

2023/02/17に公開

Goならわかるシステムプログラミング2版3章読書メモ

io.Reader

バッファの管理をしつつ、何度もRead()を呼んでデータを最後まで読み込む必要があり、少し面倒くさい。

コードのイメージはこんな感じ(読み込みが1024バイト超えたらどうするんだとか色々ツッコミどころがあるが…完全なコードはio.ReadAll()を見たほうがよい)

// 1024バイトのバッファを作る
buffer := make([[byte, 1024)
for {
	size, err := r.Read(buffer)
	if err == io.EOF {
		break
	}
}

これを毎回書くのは大変なので、golangでは読み込み周りに補助機能が多く用意されている。

Read()のブロック

golangのRead()は入力待ちでブロックしてしまう。タイムアウトも無いので入力があるまで完全に停止してしまう。他のプログラミング言語だとノンブロックなAPIが用意されているが、golangの場合は並行処理機構を使うことでブロッキングを回避する。groutineで読み込んでチャネルを通して入力を処理するようにするのが一般的。

ファイル入力

os.Create()os.Open()は内部的にはos.OpenFile()という同じ関数のエイリアスになっていて、同じシステムコールが呼ばれている。

bufio.Readernet.Connをラップする?

P42の3.4.3の以下の記述が引っかかった。

HTTPのレスポンスをパースするhttp.ReadResponse()関数が用意されています。この関数にbufio.Readerでラップしたnet.Connを渡すと、http.Response構造体のオブジェクトが返されます。

bufio.Readernet.Connをラップする必要がよくわからなかったので調べてみる。

bufio.Readerとは

バッファリングのためのio.Readerの実装だとコードコメントにある

bufio.Reader.Read()の実装

  1. Read()に渡されたpよりも内部バッファbufのサイズが小さければ直接pに書き込む
    • len(p) >= len(buf)ならわざわざ内部バッファを使う必要がない
  2. pよりもbufのサイズが大きければbufで読み込んでからp分だけコピーする
    • len(p) < len(buf)ならbufで読み取った方がたくさん読み込める

bufio.Readernet.Connでラップする理由

bufio.Reader.Read()の実装が役に立つのは、データソースから読み込む能力 > データを処理する能力の場合なのかなあと思った。データソースからはたくさん読み込めるけれど、データを処理するのはより小さい単位でやらなければならない。しかし、データを処理する単位でデータソースから読み込んでいてはIOが多すぎる。というときにbufio.Readerがうまくそれらの都合を吸収してくれる。

net.Connから読み込んだデータをどう処理するかはアプリケーションの都合によるのだと思うから、上のような差異が発生したとしてもうまくやれるようにbufio.Readerでラップしておくと問題が起こりづらいということなのかなと思った。

PNGファイルの分析

3.5.3のreadChunks()で引っかかったので詳細に読む。PNGのバイナリーフォーマットは

  • 長さ: 4バイト
  • 種類: 4バイト
  • データ: 「長さ」バイト
  • CRC: 4バイト

なので、PNGの各チャンクの概要を把握する手順としては以下になる。

  1. データの「長さ」を手に入れるために、チャンクの先頭でBigEndianとしてint32としてバイナリを解釈する
var length int32
err := binary.Read(file, binary.BigEndian, &length)
  1. 次のチャンク全体をひとかたまりにする

本書ではint64(length)+12となっているが、長さ、種類、データ、CRCをまとめたチャンクサイズになっている。

chunks = append(chunks, io.NewSectionReader(file, offset, 4+int64(length)+4+4)
  1. 次のチャンクの先頭まで移動する

binary.Read()によって現在位置が「長さ」を読み終えたところになっている。なぜ現在位置が変わっているのかわからなくてデバッグして、binary.Read()が動かしているということに気づいた。

というわけで、チャンク名(4byte)+データ長+CRC(4byte)先に移動すれば次のチャンクの先頭になる。

offset, _ = file.Seek(4+int64(length)+4, 1)

レナ・ソーダバーグさん

レナ・ソーダバーグさんの画像をサンプルに使っていましたが、その後、ご本人が技術分野での利用もモデルと同じように引退したいという表明をされたため、今回の改訂でサンプルを差し替えています。https://www.losinglena.com

知らなかった。署名しようと思ったけど、SSL証明書も切れているしFacebookのログイン周りのコードもメンテされてないみたいだ。

テキスト解析用のioReader関連機能

3.6.1のテキスト処理やや3.6.3のCSVの例を見ると、strings.Readerをそのまま使うことは少なくて、より高機能なbufio.Readercsv.Readerで更に包んであげるという感じなのかなと思った。

冗長だし最初から高機能なio.Readerの実装に文字列を渡せばいいのに…とも思ったが、一度シンプルなio.Readerでラップしておくことで、高機能なio.Readerの再利用性が上がっているのかなあと思う。文字列にしか使えないよりもio.Readerに対して扱えた方が、先の例で見たようなHTTP経由のリクエストに対して使えたりするという利点がありそう。

Q3.1

flagを使ってコマンドライン引数でファイル名を取得して、古いファイルをos.Open()、新しいファイルをos.Create()してio.Copy()で繋ぐ。

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.1/main.go

Q3.2

rand.Readerは無限長のように振る舞うのでRead()に指定したサイズ分埋めてくれる。

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.2/main.go

Q3.3

実際のファイルではなく、文字列strings.Readerを使ってzipファイルを作成するにはどうすればいいでしょうか。

これであってるのかなあ。問題文の意味としては実際のファイルを作らずに、ファイルとしてzipに含めるにはどうしたらいいかってことだと思うんだけど。

zipWriter.Create()でファイルを仮想的に作ってそのio.Writerio.Copy()strings.Readerからファイルの中身を書き込めばいいのかなあと思いました。

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.3/main.go

Q3.4

Q3.3でzip.NewWriter()os.Fileを渡していたところをhttp.ResponseWriterにすれば良さそう。

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.4/main.go

Q3.5

io.Copy()と本章で紹介した構造体のどれかを使って、

という記述が気になったけど、io.LimitReader()を使えば間接的にio.LimitedReader構造体を使ったことになる…よね?

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.5/myio.go

Q3.6

与えられている文字列から必要な文字を切り出していって、io.MultiReader()でまとめて流し込めばいいのかな。

https://github.com/roronya/go-system-programming/blob/main/chapter3/Q3.6/main.go

GitHubで編集を提案

Discussion