sliceのcapでメモリの話が少し見えた

lencap を分けて見る

前回 は、:=varconst を整理しました。

Goを触っていて一番「あー、こういうことか」となったのが、slice の lencap です。

Pythonのlistを使っていると、普段は「今何個入っているか」くらいしか意識しません。内部でどれくらい容量を確保しているかは、ほぼ考えていませんでした。

Goのsliceでは、それが見えます。

nums := make([]int, 0, 100)

これは、こういう意味です。

len = 0
cap = 100

len は今入っている数です。cap は裏側で確保している容量です。

lencapappendmake は組み込み関数です(公式)。slice自体の型は、Go言語仕様の Slice types(公式)に説明があります。

appendで容量が足りなくなるとどうなるか

足りないと新しい領域へ移る

小さいsliceで見ると、lencap の違いが分かりやすいです。

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ではそれが lencap として見える。だから「今どれだけ使っていて、裏でどれだけ確保しているか」を、必要なら見に行けるんですよね。

多く入りそうなら先に容量を取る

もちろん、最初から全部の処理でメモリ最適化を考える必要はなさそうです。ただ、大量に入ることが分かっているなら、先に容量を確保できます。

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で形を決める に進みます。