はじめに (対象読者・この記事でわかること)
この記事は、Go言語でプログラムを書いている方や、これから学び始めようと考えている方を対象としています。特に、スライスの一部を取り出す「s[n:m]」という式の書き方や挙動に不安がある人に最適です。この記事を読むと、スライス式の基本的な動作、インデックスの取り扱い、境界チェックの仕組み、実務でよく起こるバグとその対策まで、一通り理解できるようになります。Goで安全かつ効率的にデータを切り取るコツを身につけましょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Go言語の基本的な文法(変数宣言、配列・スライスの作成)
- 関数や構造体の定義・呼び出しの経験
Goのスライス式 s[n:m] の概要と仕様
Go のスライスは「参照型」の一種で、配列の一部を指し示す軽量なデータ構造です。スライス式 s[n:m] は、元のスライス s から 開始インデックス n(含む) から 終了インデックス m(除く) までの要素を新しいスライスとして切り出します。以下が重要なポイントです。
-
インデックスは 0 から始まる
nとmはどちらも 0 以上の整数です。n == mの場合は長さ 0 のスライスが生成されます。 -
省略可能なインデックス
-s[:m]はnが 0 の省略形。
-s[n:]はmがlen(s)の省略形。
-s[:]は元スライスと同じ長さ・容量のコピー(実際には同一の配列を参照)。 -
境界チェックはランタイムで行われる
nまたはmが0 ≤ n ≤ m ≤ len(s)の範囲外の場合、実行時にパニックが発生します。コンパイル時にチェックできないため、注意が必要です。 -
容量(capacity)
新しいスライスの容量はlen(s) - nとなります。cap(s[n:m])がlen(s) - nになることを覚えておくと、appendでのリサイズ挙動を予測しやすくなります。
この仕組みを正しく理解すれば、配列・スライス操作で頻繁に遭遇する「要素の取り出し」や「部分集合の作成」を安全かつ効率的に実装できます。
実践:スライス式を使った具体的なコード例と落とし穴
以下では、実際のコードを交えてスライス式の使い方と、つまずきやすいポイントを解説します。
ステップ1: 基本的なスライス取得
Gopackage main import ( "fmt" ) func main() { // 元となるスライス s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} fmt.Println("元スライス:", s) // s[2:5] => インデックス 2,3,4 の要素を取得 sub := s[2:5] fmt.Println("s[2:5] =", sub) // => [2 3 4] // 省略形 head := s[:3] // 0,1,2 tail := s[7:] // 7,8,9 fmt.Println("head =", head, "tail =", tail) }
ポイント
- sub は元配列の要素 2~4 を参照しているだけで、コピーは行われません。
- head と tail はそれぞれ異なる開始位置を持つスライスですが、内部的に同じ配列を共有しています。
ステップ2: 切り出し範囲の応用と容量の確認
Gofunc sliceInfo(s []int, n, m int) { sub := s[n:m] fmt.Printf("sub = %v, len = %d, cap = %d\n", sub, len(sub), cap(sub)) } func main() { data := []int{10, 20, 30, 40, 50, 60, 70} sliceInfo(data, 1, 4) // => [20 30 40] len=3 cap=6 }
解説
- len(sub) は m-n(=3)です。
- cap(sub) は元スライス data の末尾までの要素数、すなわち len(data)-n(=6)になります。
- この情報は append の挙動を予測するときに重要です。容量が十分にある場合は再割り当てが行われず、元配列がそのまま利用されます。
ハマった点やエラー解決
| 例 | 発生したエラー | 原因 | 解決策 |
|---|---|---|---|
s[5:3] |
runtime panic: slice bounds out of range [5:3] | 終了インデックスが開始インデックスより小さい | インデックス順序を正しく保つ。必要なら if n > m { n, m = m, n } で入れ替える |
s[0:len(s)+1] |
runtime panic: slice bounds out of range [0:len+1] | 終了インデックスがスライス長を超えている | len(s) 以下に収めるか、append で要素を追加した後に再スライス |
s[-1:2](負のインデックス) |
コンパイルエラー: invalid integer literal | Go では負のインデックスは許容されない | 正のインデックスに変換、または len(s)+offset の形で計算 |
具体的な対策コード例
Gofunc safeSlice(s []int, n, m int) []int { // 負のインデックスは自動的に len(s)+offset に変換 if n < 0 { n = len(s) + n } if m < 0 { m = len(s) + m } // 境界チェック if n < 0 { n = 0 } if m > len(s) { m = len(s) } if n > m { n, m = m, n // 順序を修正 } return s[n:m] }
解決策のまとめ
- インデックスは常に 0 以上、
len(s)以下であることを保証する。 - 省略形(
s[:m],s[n:])はコードをシンプルにするが、意図しない長さになるケースがあるので、lenとcapをデバッグ出力で確認すると安心です。 - 負のインデックスはサポートされていないので、独自にラップする関数(上記
safeSlice)を用意すると再利用性が向上します。 - パニック回避のためのテストを必ず書く。特に外部入力に基づくインデックス計算は、境界チェックを怠ると致命的です。
まとめ
本記事では、Go 言語のスライス式 s[n:m] の基本的な動作、インデックスの省略形、容量の関係、そして実務でありがちな落とし穴とその回避策を詳細に解説しました。
- スライス式は「開始は含む、終了は除く」 というルールで動作し、境界外はランタイムパニックになる。
- 省略形 を上手く使うとコードが簡潔になるが、
lenとcapの把握は必須。 - エラー回避策(インデックス正規化、境界チェック、ラップ関数)を実装すれば、バグの少ない安全なコードが書ける。
この記事を通じて、読者は Go のスライス操作を正しく、かつ安全に扱えるようになり、実務でのデータ加工やアルゴリズム実装が格段に楽になります。次回は「スライスと append の内部実装」や「メモリ効率を考慮した大規模データ処理」について掘り下げる予定です。
参考資料
- Go言語公式ドキュメント – Slices: usage and internals
- Effective Go – Slices, arrays, and maps
- Go Playground – スライス式のサンプルコード
- 「Goプログラミング実践入門」著:山田祥寛(技術評論社)
