<Goのパッケージ放浪記> ioパッケージに定義されている「Writerインターフェイス」について
今日は前回のReaderインターフェイスに続き、ioパッケージのWriterインターフェイスまわりを覗いていきます。
Writerインターフェイス
WriterインターフェイスはReaderのすぐ下で定義されています。
// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
Write(p []byte) (n int, err error)
}
実装は以下のような挙動が求められています。
- 引数に与えられた p の内容を len(p) バイトまでデータストリームに書き込む
- 書き込んだバイト数と、書き込みを停止させる原因となったエラーを返す
- 書き込んだバイト数は、 0 <= n <= len(p) 以下になる
- 一時的であっても、 p の内容を変更してはいけない
- 実装するときに p を保持してはいけない
Writerインターフェイスの実装を確認する
Writerインターフェイスの実装として今回は、bytesパッケージのBufferを調査して実装を探っていきます。
// Write appends the contents of p to the buffer, growing the buffer as
// needed. The return value n is the length of p; err is always nil. If the
// buffer becomes too large, Write will panic with ErrTooLarge.
func (b *Buffer) Write(p []byte) (n int, err error) {
// 最後の読み込み操作の状態を設定する、Unread系の動作に必要らしい
b.lastRead = opInvalid
// キャパシティが足りているか確認して足りていない場合はokがfalseを返す
// mは書き込まれるインデックスを返す
m, ok := b.tryGrowByReslice(len(p))
if !ok {
// キャパシティが足らない場合は、キャパシティを変更する
m = b.grow(len(p))
}
// bのmから最後までにpをコピーする
return copy(b.buf[m:], p), nil
}
実際に両方のメソッドを確認してみます。
// tryGrowByReslice is a inlineable version of grow for the fast-case where the
// internal buffer only needs to be resliced.
// It returns the index where bytes should be written and whether it succeeded.
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
// nが空いているキャパシティに入りきる場合はバッファの内容を最初からl+nまでにする(n分だけ0で埋めてlenを伸ばす)
if l := len(b.buf); n <= cap(b.buf)-l {
b.buf = b.buf[:l+n]
return l, true
}
return 0, false
}
残りのキャパシティが書き込むバイトの要素数以上の場合、つまり、キャパシティが十分にある場合は、 b.buf の最初から、 l+n までの長さのスライスにして、 l を書き込み位置のインデックスとして返しています。
残りのキャパシティが十分の場合は、アロケートする必要がないのでスライス操作で高速に処理できることがわかります。
次はgrowメソッドを確認します。
const maxInt = int(^uint(0) >> 1)
// Len returns the number of bytes of the unread portion of the buffer;
// b.Len() == len(b.Bytes()).
func (b *Buffer) Len() int { return len(b.buf) - b.off }
// makeSlice allocates a slice of size n. If the allocation fails, it panics
// with ErrTooLarge.
func makeSlice(n int) []byte {
// If the make fails, give a known error.
defer func() {
if recover() != nil {
panic(ErrTooLarge)
}
}()
return make([]byte, n)
}
// grow grows the buffer to guarantee space for n more bytes.
// It returns the index where bytes should be written.
// If the buffer can't grow it will panic with ErrTooLarge.
func (b *Buffer) grow(n int) int {
m := b.Len()
// バッファに未読がなく、オフセットが0以外の場合はリセットする
// If buffer is empty, reset to recover space.
if m == 0 && b.off != 0 {
b.Reset()
}
// nが残りのキャパシティに入りきる場合はバッファのlenを伸ばす
// Writeメソッドからgrow関数が呼び出された場合はWriteメソッド側で先にチェックされている
// Try to grow by means of a reslice.
if i, ok := b.tryGrowByReslice(n); ok {
return i
}
// バッファがnilで、nがsmallBufferSize以下の場合は、smallBufferSizeでバッファを作成する
if b.buf == nil && n <= smallBufferSize {
b.buf = make([]byte, n, smallBufferSize)
return 0
}
c := cap(b.buf)
// nがキャパシティの半分から未読のバイト数を引いたもの以下の場合
// バッファに現在のオフセットから最後までをコピーする
if n <= c/2-m {
// We can slide things down instead of allocating a new
// slice. We only need m+n <= c to slide, but
// we instead let capacity get twice as large so we
// don't spend all our time copying.
// ここで読み込み済みのバイトは消える
copy(b.buf, b.buf[b.off:])
// キャパシティが、Intの最大値からキャパシティとnを引いたものより大きい場合はエラー
} else if c > maxInt-c-n {
panic(ErrTooLarge)
// キャパシティが小さい場合は、現在のキャパシティの2倍にnを加えたサイズのスライスをバッファにしてコピーする
} else {
// Not enough space anywhere, we need to allocate.
buf := makeSlice(2*c + n)
// ここで読み込み済みのバイトは消える
copy(buf, b.buf[b.off:])
b.buf = buf
}
// Restore b.off and len(b.buf).
b.off = 0
// バッファの要素数を未読バイトとnの合計に設定する
b.buf = b.buf[:m+n]
return m
}
growメソッドはバッファのサイズを伸ばして書き込むインデックスを返します。
growメソッドの内部を見ていく前に、growメソッドが利用しているメソッドや変数を確認しておきます。
Lenメソッドは、バッファのまだ読み込まれていないバイト数(以下、未読バイト数)を返します。
maxIntは、一度符号無しの32ビットもしくは64ビットの「0」からビットを反転させてビットをすべて「1」にし、符号分の最初の1ビットをシフトしたのちに、int型に変換してIntの最大数を保存しているようです。
64ビットの場合のイメージは以下のようになります。
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 => 0
ビットを反転
1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 => 18446744073709551615
最初のビットをシフトして「0」にする
0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 => 9223372036854775807
では、growメソッドの内部を覗いていきます。 前半はバッファが空だったり先ほどの tryGrowByReslice で対応できる場合のチェックをしています。
後半からは、 tryGrowByReslice では対応できない場合、すなわちキャパシティが十分に足りない場合の処理になっているようです。
ここからは具体的にバッファのスライスの状態とその処理を見ていきます。 下の図は、「o」がキャパシティ「x」が未読バイト、「-」が既読バイトのそれぞれ1バイトを表しています。
最初の例として、c=12、l=9、m=7の場合の状態を図で表してみます。
c=12(キャパシティ)
l=9(要素数、実際に入っているバイト数)
m=2(未読のバイト数)
oooo oooo oooo
---- ---x x
キャパシティが12バイトあり、そのうち9バイトにデータが入っていて、7バイトまで既読の状態です。
この状態で4バイト追加しようとすると、 tryGrowByReslice では、空きのキャパシティが3バイトなので対応できません。
そこで、 n <= c/2-m が評価され true になります。
キャパシティの半分から、未読バイトを差し引いてもnより大きい場合、つまり、未読バイトと n の合計の二倍のキャパシティが残っている場合は、 既読バイトを捨て、以下のように現在の読み込み済みのインデックスから最後までをコピーしています。
oooo oooo oooo
xx
この状態だと、 n バイト書き込まれても二倍のキャパシティを確保できます。
次に、 n <= c/2-m が に評価される場合を見ていきます。 この状態は、どうやってもキャパシティが十分に足らない場合です。
c=12(キャパシティ)
l=9(要素数、実際に入っているバイト数)
m=7(未読のバイト数)
oooo oooo oooo
--xx xxxx x
この場合は、既読バイトを捨ててもキャパシティが足らないので、アロケートして新たなスライスを作成しています。
まとめ
Writerインターフェイスの実装を覗いていたのですが、いつのまにかBufferの実装の調査になっていました。不思議。
このあたりから、処理の内容は理解できるが、何をしたいのかが理解できない箇所が増え、それを調べたり考えるのに時間がかかりました。
とはいえ、Bufferはバイトのスライスを直接ユーザが制御せずとも、効率よく扱えるように実装されていることがわかりましたし、Intの最大値の出し方や、キャパシティの成長方法などとても学ぶものが多かったです。 今後の実装の参考になりそうです。
この記事を書いた人
-
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この執筆者の最新記事
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー