sliceのcapでメモリの話が少し見えた
len と cap を分けて見る
前回 は、:=、var、const を整理しました。
Goを触っていて一番「あー、こういうことか」となったのが、slice の len と cap です。
Pythonのlistを使っていると、普段は「今何個入っているか」くらいしか意識しません。内部でどれくらい容量を確保しているかは、ほぼ考えていませんでした。
Goのsliceでは、それが見えます。
nums := make([]int, 0, 100)
これは、こういう意味です。
len = 0
cap = 100
len は今入っている数です。cap は裏側で確保している容量です。
len、cap、append、make は組み込み関数です(公式)。slice自体の型は、Go言語仕様の Slice types(公式)に説明があります。
appendで容量が足りなくなるとどうなるか
足りないと新しい領域へ移る
小さいsliceで見ると、len と cap の違いが分かりやすいです。
nums := make([]int, 0, 3)
fmt.Println(len(nums), cap(nums)) // 0 3
nums = append(nums, 10)
nums = append(nums, 20)
fmt.Println(len(nums), cap(nums)) // 2 3
append で要素を増やしていき、今の cap で足りなくなると、Goがより大きい領域を確保して、古い要素をコピーして、新しいsliceを返します。
今の cap では足りない
↓
もっと大きい領域を確保する
↓
古い要素をコピーする
↓
新しい要素を追加する
↓
新しいsliceを返す
この話で、メモリの話が少しだけ自分の中につながりました。
Pythonでも裏側では似たようなことをしているはずです。ただ、Goではそれが len と cap として見える。だから「今どれだけ使っていて、裏でどれだけ確保しているか」を、必要なら見に行けるんですよね。
多く入りそうなら先に容量を取る
もちろん、最初から全部の処理でメモリ最適化を考える必要はなさそうです。ただ、大量に入ることが分かっているなら、先に容量を確保できます。
users := make([]User, 0, len(rows))
これは「最終的に len(rows) 件くらい入りそうだから、先に場所を取っておく」という書き方です。
ここでようやく、メモリを気にする人たちが「この時点で全件持ってますか?」とか「コピー起きますか?」と聞く感覚が、少しだけ分かった気がしました。
変数のサイズだけを見ているというより、その変数が裏側でどれだけのデータを持っているか、どこで確保して、どこでコピーされるかを見ているんですね。
sliceは切り出してもコピーされない
切り出しは同じ配列を見る
sliceでもうひとつ大事そうだったのが、切り出しはコピーではないというところです。
letters := []string{"a", "b", "c", "d"}
left := letters[:2] // [a b]
mid := letters[1:3] // [b c]
right := letters[2:] // [c d]
letters[1:3] は、インデックス1から始めて、3の手前までです。
この [low:high] のような書き方は、Go言語仕様の Slice expressions(公式)に載っています。
index: 0 1 2 3
letters: [a b c d]
letters[1:3] => [b c]
ここまでは分かりやすいです。問題は、これがコピーではなく、同じ裏側の配列を見ているという点です。
letters := []string{"a", "b", "c", "d"}
left := letters[:2]
left[0] = "x"
fmt.Println(letters) // [x b c d]
fmt.Println(left) // [x b]
left を変えると、元の letters も変わります。読み取りだけなら便利ですが、変更や append が絡むと普通にハマりそうです。
独立したsliceが必要なら copy
独立した別sliceがほしいなら、copy を使います。
letters := []string{"a", "b", "c", "d"}
left := make([]string, 2)
copy(left, letters[:2])
left[0] = "x"
fmt.Println(letters) // [a b c d]
fmt.Println(left) // [x b]
copy も組み込み関数です(公式)。
このあたりは、Goがメモリを勝手に全部隠すのではなく、必要なところは見えるようにしている感じがありました。便利だけど、知らないと怖い。こういうところに言語の考え方が出るんだなと思いました。
次は APIを書くならmapよりstructで形を決める に進みます。