Golangのio.ReadAllからsliceの挙動を学ぶ
io.ReadAllの実装
Goのio
パッケージ関数ReadAll
は
io.Reader
インターフェースを引数に受け取り、([]byte, error)
を返却します。
(Go1.18.0のコードから抜粋)
関数内部で[]byte
を生成していますが、このsliceに対して行われている操作に学びが多いと思ったので、
この関数の挙動をsliceを中心にまとめたいと思います。
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}
io.ReadAllにおけるsliceの扱われ方
(1)sliceをmake関数で初期化して長さ・容量を指定する
まずは1行目で[]byte
の変数b
を初期化しています。
// io.ReadAll L1
b := make([]byte, 0, 512)
make
関数はslice, map, channel のいずれかの初期化時に利用できます。
func make(t Type, size ...IntegerType) Type
sliceの場合の挙動は以下になります。
(make
関数のコメントから抜粋)
The size specifies the length. The capacity of the slice is
equal to its length. A second integer argument may be provided to
specify a different capacity; it must be no smaller than the
length. For example, make([]int, 0, 10) allocates an underlying array
of size 10 and returns a slice of length 0 and capacity 10 that is
backed by this underlying array.
-
size
でsliceの長さ(length)を指定する - sliceの容量(capacity)は長さと同じになる
- 別の容量を指定する場合は2番目の整数引数を指定する(長さ以上の値を指定)
-
make([]int, 0, 10)
は基になるサイズ10の配列を割り当て、長さ:0、容量:10のsliceを返却する
つまり、io.ReadAll
の1行目で長さ0、容量512のsliceを作っていることになります。
これはサイズ512の配列がメモリを確保したということでもあるので、
長さ512まではsliceのためのメモリ再割り当ての必要がなく、リソースを効率的に活用できます。
動作確認のために以下のコードを実行します。
func sample1() {
s0 := []int{} // makeを使わない(length:0)
s1 := []int{0, 0, 0, 0, 0} // makeを使わない(length:5)
s2 := make([]int, 0, 10) // makeを使う(length:0, capacity:10)
s3 := make([]int, 5, 10) // makeを使う(length:5, capacity:10)
fmt.Printf("[s0] len: %d, cap: %d, s: %d\n", len(s0), cap(s0), s0)
fmt.Printf("[s1] len: %d, cap: %d, s: %d\n", len(s1), cap(s1), s1)
fmt.Printf("[s2] len: %d, cap: %d, s: %d\n", len(s2), cap(s2), s2)
fmt.Printf("[s3] len: %d, cap: %d, s: %d\n", len(s3), cap(s3), s3)
}
結果は以下の通りです。
make
で容量を長さ以上に指定しても、長さの分しかslice内の値は出力されません。
また、slice内の値はゼロ値で初期化されています。
make
を使わずに初期化した場合、長さと容量は一致しているため、
sliceに1つでも値を追加すると新たな配列を作成してsliceに割り当てなおす必要があり、結果としてメモリの再割り当てが発生します。
sliceの最終的な長さがほぼ確定している場合や、ある程度大きくなることが予想される場合は、
make
関数で容量を指定してsliceを作成したほうが良さそうです。
[s0] len: 0, cap: 0, s: []
[s1] len: 5, cap: 5, s: [0 0 0 0 0]
[s2] len: 0, cap: 10, s: []
[s3] len: 5, cap: 10, s: [0 0 0 0 0]
(2)append関数を使ってsliceの容量を拡張
続いて2行目以降はfor文になっており、
3-6行目では、b []byte
が容量いっぱいまで埋まった際に、append
関数でb
を拡張しています。
具体的にはappend
で1つだけ要素を追加することで容量を拡張させた後、
元の長さのsliceとして切り取っています。
容量をどれだけ拡張するかはappend
の挙動に任せています。
// io.ReadAll L3-6
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
同様の処理を行った際の動作確認のため、以下のコードを実行します。
func sample2() {
b := make([]byte, 0)
for i := 0; i < 5000; i++ {
if len(b) == cap(b) {
fmt.Printf("[before|%p] len: %d, cap: %d\n", b, len(b), cap(b))
b = append(b, 0)[:len(b)]
fmt.Printf("[after |%p] len: %d, cap: %d\n", b, len(b), cap(b))
}
b = append(b, 0)
}
}
結果は以下の通りです。
-
len(b)
とcap(b)
が同じ状態でappendを行うと、b
のアドレスが変わり、容量も増える- 容量は8~512までは2倍ずつ増える
- それ以降は増加率が徐々に低下する
-
len(b)
とcap(b)
が同じ状態でappend(b, 0)[:len(b)]
とすると、容量が拡張され長さはそのまま
[before|0x552090] len: 0, cap: 0
[after |0xc0000140a8] len: 0, cap: 8
[before|0xc0000140a8] len: 8, cap: 8
[after |0xc0000140c0] len: 8, cap: 16
[before|0xc0000140c0] len: 16, cap: 16
[after |0xc000018120] len: 16, cap: 32
[before|0xc000018120] len: 32, cap: 32
[after |0xc00001a140] len: 32, cap: 64
[before|0xc00001a140] len: 64, cap: 64
[after |0xc000022080] len: 64, cap: 128
[before|0xc000022080] len: 128, cap: 128
[after |0xc000092000] len: 128, cap: 256
[before|0xc000092000] len: 256, cap: 256
[after |0xc000094000] len: 256, cap: 512
[before|0xc000094000] len: 512, cap: 512
[after |0xc000096000] len: 512, cap: 896
[before|0xc000096000] len: 896, cap: 896
[after |0xc000098000] len: 896, cap: 1408
[before|0xc000098000] len: 1408, cap: 1408
[after |0xc00009c000] len: 1408, cap: 2048
[before|0xc00009c000] len: 2048, cap: 2048
[after |0xc00009e000] len: 2048, cap: 3072
[before|0xc00009e000] len: 3072, cap: 3072
[after |0xc0000a4000] len: 3072, cap: 4096
[before|0xc0000a4000] len: 4096, cap: 4096
[after |0xc0000a6000] len: 4096, cap: 5376
(3)配列に要素を追加してからsliceの長さを拡張する
最後に7~8行目でio.Reader
からの読み込んだ結果を[]byte
で受け取っています。
// io.ReadAll L7-8
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
7行目で使われているio.Reader
は以下のようにRead
関数を持ったinterfaceになっています。
//io package
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
関数は以下のように実装するという指針があります。
- バイト数が
len(p)
分だけ読み込んで、p
に対して読み込んだ値を格納する - 読み込んだバイト数
n
と、発生したエラーerr
を返却する- 0 <=
n
<=len(p)
- 0 <=
7行目のb[len(b):cap(b)]
はb
が長さ(len)として確保している分の次のインデックスから、
容量(cap)までを別のsliceとして切り出したものです。
その後、8行目のb = b[:len(b)+n]
で、
b
の長さ(len)をr.Read
で読み込んだバイト数の分だけ増やしています。
この実装から、以下の挙動がわかります。
- スライス
s
に対して、len(s)
<cap(s)
になっている場合もs[len(s):cap(s)]
という形でs
の元になっている配列の一部を別のスライスとして切り出すことができる - 別のスライスとして切り出した
s[len(s):cap(s)]
に格納された値はs
の元配列と同じ配列に格納されているので、s = s[:cap(s)]
とすればsの一部として参照可能
こちらも動作確認してみます。
func sample3() {
s := make([]int, 5, 10) //len:5, cap:10
fmt.Printf("s:%d\n", s)
s2 := s[len(s):cap(s)]
for i, _ := range s2 {
s2[i] = i + 1
}
fmt.Printf("s2:%d\n", s2)
s = s[:len(s)+len(s2)]
fmt.Printf("s:%d\n", s)
}
結果は以下のようになりました。
s:[0 0 0 0 0]
s2:[1 2 3 4 5]
s:[0 0 0 0 0 1 2 3 4 5]
以上の(1)~(3)の操作を行うことで、sliceの容量を適宜拡張しながら読み込みが実行されていました。
簡単な小ネタのような内容でしたが、シンプルにGoのコードを書くヒントにもなったので、
今後の参考にしていきたいと思います。
Discussion