はじめに
Goでスライスを扱っていると、make の使い方が微妙に違う書き方を見かけることがあります。どちらも正しく動くのに、なぜ書き分けるのか。最初はあまり気にしていませんでしたが、ある程度コードを書いていくと、この違いが設計の意図を反映していることに気づきました。
make関数の基本
Goの make は、スライス・マップ・チャネルを初期化するための組み込み関数です。スライスに対しては次のように使います。
make([]T, length, capacity)
- length(len):スライスの現在の要素数。インデックスでアクセスできる範囲。
- capacity(cap):内部配列として確保されたメモリの大きさ。lenを超えた追加に備えるバッファ。
capを省略した場合、cap = len になります。言い換えると、lenは「今使っている領域」、capは「確保されている最大容量」です。この2つを意識するだけで、スライスの挙動がかなり読めるようになります。
パターン①:lenを指定する
s := make([]int, 5)
// len=5, cap=5
// s = [0, 0, 0, 0, 0]
特徴
make([]int, 5) は、長さ5のスライスを作ります。各要素はゼロ値で初期化されます。s[0] から s[4] まで最初からアクセス可能な状態です。
使いどころ
要素数がはっきり決まっていて、インデックスで直接代入していく場合に適しています。
results := make([]int, len(input))
for i, v := range input {
results[i] = v * 2
}
注意点:appendとの関係
ここで初心者がよくやるミスがあります。make([]int, 5) で作ったスライスに対して append を使うと、既存の5要素の後ろに追加されます。
s := make([]int, 5)
s = append(s, 1)
// s = [0, 0, 0, 0, 0, 1] ← 意図と違う結果になることがある
len > 0 のスライスはすでに要素が存在している状態なので、append は既存要素を置き換えません。インデックス代入と append を混在させると、予期しない結果になりやすいので注意が必要です。
「appendしたはずなのに先頭が0のまま」という混乱はこれが原因です。append はlenの末尾に追加する操作であり、既存要素を上書きする手段ではありません。このパターンを使うなら、インデックス代入に統一するのが無難です。
パターン②:len=0 + capを指定する
s := make([]int, 0, 5)
// len=0, cap=5
// s = [] ← 空
特徴
長さゼロ、つまり要素が何もない状態で始まります。ただし内部では5要素分のメモリがすでに確保されています。s[0] にアクセスしようとするとパニックになります。
使いどころ
append で要素を積み上げていく場合に適しています。
filtered := make([]int, 0, len(input))
for _, v := range input {
if v > 0 {
filtered = append(filtered, v)
}
}
メリット:メモリ再確保の回避
append はスライスのlenがcapを超えると、新しい内部配列を確保してコピーが走ります。事前に cap を指定しておくことで、その再確保を抑えられます。要素数がある程度予測できる場合、このパターンはパフォーマンス上の利点があります。
2つの違いの整理
make([]T, n) |
make([]T, 0, n) |
|
|---|---|---|
| 初期状態 | n個のゼロ値要素あり | 要素なし(空) |
| アクセス | インデックスで即アクセス可 | appendで追加してから |
| appendの挙動 | 既存要素の後ろに追加 | 先頭から追加される |
| 主な用途 | インデックス代入 | appendによる逐次追加 |
使い分けの指針
実務では次のように考えると迷いにくいです。判断の軸はシンプルで、要素数が固定かどうかです。
インデックスで書き込むなら make([]T, n)
要素数が確定していて、results[i] = ... のように位置を指定して代入するケース。変換処理やマッピング処理に多いパターンです。入力と出力の長さが1対1で対応する処理が典型です。
appendで積み上げるなら make([]T, 0, n)
フィルタリングや条件付き収集のように、最終的な要素数が実行前には確定しないケース。要素数が可変なときはこちらを選ぶと自然です。ただし上限の見当がつくなら cap を渡しておくと効率がよいです。
どちらでも動く場面はありますが、意図を明確にするためにも使い分けを意識するとよいでしょう。
まとめ
make の第2引数と第3引数は、スライスの「現在の状態」と「将来の余裕」を表しています。len > 0 は要素がすでに存在する状態であり、len = 0 は空の器として始まる状態です。この違いを理解していると、append が絡んだバグを未然に防げます。
スライスの初期化は些細に見えますが、どう使うかを先に決めてから初期化するという順序を意識すると、コードの意図が伝わりやすくなります。

コメント