Go言語のスライスを初心者向けに徹底解説!基本から使い方まで

2025年4月17日木曜日

Go言語

Go言語のスライス、プログラミング学習で出会うと「ん?配列と何が違うの?」って思いますよね!

実はGo言語を書く上で、スライスはめちゃくちゃ便利で、避けては通れない道なんです。
配列みたいに最初に数を決めなくても、後からデータを増やしたりできる、柔軟なやつなんですよ。

この記事では、Go言語のスライスの基本的なところから、実際の使い方、ちょっとした注意点まで、「なるほど!」と思えるように、サンプルコードをたっぷり紹介して解説していきます。

この記事で学べること

  • スライスが何なのか、配列との違い
  • スライスの作り方(色々なパターン!)
  • 作ったスライスの中身を見たり、数を数えたりする方法
  • スライスにデータを追加する方法(append関数)
  • スライスの一部だけを取り出す方法
  • スライスのデータを丸ごとコピーする方法(copy関数)
  • スライスを使うときに気をつけるポイント

Go言語のスライスとは?配列との基本的な違い

Go言語のスライスは、一言でいうと「可変長の配列」みたいなものです。

データを順番に並べて格納する点は配列と同じなんですが、大きな違いは、後から要素の数を変えられる点にあります。

配列だと、最初に「要素は5個ね!」と決めたら、後から6個に増やすことはできません。
でもスライスなら、「とりあえず3個で作るけど、後で増えるかも…」という状況に対応できるんです。便利ですよね!

Go言語では、この柔軟性の高さから、固定長の配列よりもスライスの方が圧倒的によく使われます。
データを扱うプログラムでは、最初からデータ量が確定していることの方が少ないですからね。

// 配列とスライスのイメージ図

【配列】 (長さ固定)
[ 要素1 | 要素2 | 要素3 ]  <-- 長さ3で固定!増やせない

【スライス】 (長さ可変)
[ 要素1 | 要素2 | 要素3 ]  <-- 今は長さ3だけど...
↓ (要素を追加!)
[ 要素1 | 要素2 | 要素3 | 要素4 ] <-- 長さ4に増えた!

こんな感じで、必要な分だけ伸び縮みしてくれるのがスライスの良いところです。

Go言語のスライスの基本的な宣言方法と初期化

じゃあ、実際にスライスをどうやって作るのか見ていきましょう。
いくつか作り方があるので、順番に紹介しますね。

スライスリテラルを使った初期化

一番お手軽なのが、スライスリテラルを使う方法です。
`[]データ型{要素1, 要素2, ...}` という形で、中に入れるデータを直接書いて作れます。

package main

import "fmt"

func main() {
	// int型のスライスを作成し、初期値を設定
	numbers := []int{10, 20, 30}
	fmt.Println(numbers) // 中身を表示

	// string型のスライスも同じように作れる
	fruits := []string{"apple", "banana", "cherry"}
	fmt.Println(fruits) // 中身を表示
}

実行結果はこうなります。

[10 20 30]
[apple banana cherry]

中身を決め打ちで作りたいときに便利ですね。

`make`関数を使ったスライス作成

もう一つの代表的な作り方が、`make`関数を使う方法です。
`make([]データ型, 長さ, 容量)` という形で使います。

ここで新しい言葉、「長さ」と「容量」が出てきました。

  • 長さ (length)
    スライスに現在入っている要素の数。`len()`関数で取得できます。
  • 容量 (capacity)
    スライスが、内部的にどれくらいの要素を保持できる大きさを持っているか。`cap()`関数で取得できます。長さ以上、容量以下の範囲で要素を追加する際は、メモリの再確保が行われず効率的です。

容量は、スライスのパフォーマンスに関わる少し内部的な話ですが、`make`で最初に指定しておくと、後で要素が増えるときに効率よくメモリを使える、というメリットがあります。
容量を指定しない場合は、長さと同じ値が容量になります (`make([]データ型, 長さ)`)。

package main

import "fmt"

func main() {
	// 長さ3、容量5のint型スライスを作成 (要素はゼロ値で初期化される)
	// 容量を指定することで、将来4つ目、5つ目の要素を追加する際に効率的になる
	s1 := make([]int, 3, 5)
	fmt.Println("s1:", s1)
	fmt.Println("長さ:", len(s1)) // len()で長さを取得
	fmt.Println("容量:", cap(s1)) // cap()で容量を取得

	fmt.Println("---")

	// 容量を省略すると、長さと同じ容量になる (この場合は長さ3、容量3)
	s2 := make([]int, 3)
	fmt.Println("s2:", s2)
	fmt.Println("長さ:", len(s2))
	fmt.Println("容量:", cap(s2))
}

実行結果

s1: [0 0 0]
長さ: 3
容量: 5
---
s2: [0 0 0]
長さ: 3
容量: 3

`make`で作ると、最初はデータ型のゼロ値(intなら0、stringなら空文字など)で初期化されます。

// makeで作ったスライスの内部イメージ図 (make([]int, 3, 5) の場合)

+-----------+
| スライス変数 |
+-----------+
    |
    V
+-----------------+
| 内部配列へのポインタ | ----> [ 0 | 0 | 0 | (未使用) | (未使用) ]
+-----------------+
| 長さ = 3        | // 現在の要素数
+-----------------+
| 容量 = 5        | // 確保されているメモリ領域のサイズ
+-----------------+

こんなイメージです。長さは3だけど、内部的には5個分のスペースが用意されている状態ですね。

Go言語のスライスの基本的な使い方 - 要素へのアクセスと操作

スライスを作ったら、次はその中身を使ってみましょう!

要素へのアクセスは、配列と同じようにインデックスを使います。インデックスは`0`から始まることに注意してくださいね。

そして、さっき`make`のところでチラッと出てきた`len()`と`cap()`も、スライスの状態を知るのによく使います。

package main

import "fmt"

func main() {
	numbers := []int{10, 20, 30, 40, 50}

	// インデックスを使って要素にアクセス (0から始まる!)
	fmt.Println("最初の要素:", numbers[0]) // 0番目なので 10
	fmt.Println("3番目の要素:", numbers[2]) // 2番目なので 30

	// 要素の値を変更することもできる
	numbers[1] = 25
	fmt.Println("変更後のスライス:", numbers)

	// len() で要素数を取得
	fmt.Println("要素数:", len(numbers))

	// cap() で容量を取得 (スライスリテラルの場合、長さと同じになることが多い)
	fmt.Println("容量:", cap(numbers))
}

実行結果

最初の要素: 10
3番目の要素: 30
変更後のスライス: [10 25 30 40 50]
要素数: 5
容量: 5

簡単ですね!配列と同じ感覚でアクセスできます。

スライスの要素を追加する`append`関数の使い方

さあ、スライスの真骨頂、要素の追加です!
これには`append`関数を使います。Go言語のスライス操作で最もよく使う関数の一つなので、しっかり覚えましょう。

`append(元のスライス, 追加したい要素1, 追加したい要素2, ...)` のように使います。
`append`は元のスライスを変更するのではなく、要素が追加された新しいスライスを返すので、普通は元の変数に再代入して使います。

package main

import "fmt"

func main() {
	s := []int{1, 2}
	fmt.Println("元のスライス:", s, "長さ:", len(s), "容量:", cap(s))

	// 要素を1つ追加
	s = append(s, 3)
	fmt.Println("1つ追加後 :", s, "長さ:", len(s), "容量:", cap(s))

	// 要素を複数まとめて追加
	s = append(s, 4, 5)
	fmt.Println("複数追加後:", s, "長さ:", len(s), "容量:", cap(s))

	// 別のスライスの要素をまとめて追加 (スライス名の後に ... を付ける)
	s2 := []int{6, 7}
	s = append(s, s2...)
	fmt.Println("結合後    :", s, "長さ:", len(s), "容量:", cap(s))
}

実行結果 (容量の変化に注目!)

元のスライス: [1 2] 長さ: 2 容量: 2
1つ追加後 : [1 2 3] 長さ: 3 容量: 4  // 容量が足りなくなったので、倍(くらい)に増えた!
複数追加後: [1 2 3 4 5] 長さ: 5 容量: 8 // さらに追加。容量はまだ余裕あり(内部で倍々に増えていくことが多い)
結合後    : [1 2 3 4 5 6 7] 長さ: 7 容量: 8 // さらに追加。容量はまだ余裕あり

ここで面白いのが容量の変化です。

`append`した結果、元のスライスの容量を超える場合、Go言語は内部でより大きな新しい配列を用意し、そこに元の要素をコピーしてから新しい要素を追加します。そして、スライスはその新しい配列を指すように変更されます。

だから`append`後の容量が、元の容量の倍(またはそれに近い値)になったりするんですね。このおかげで、要素追加のパフォーマンスが良くなっています。

スライスの部分的な切り出し(サブスライス)

スライスの一部だけを取り出して、新しいスライスを作ることもできます。
これをサブスライスとか、スライス式とか言います。

`元のスライス[開始インデックス:終了インデックス]` のように書きます。
注意点として、終了インデックスの要素は含まれないこと、そして元のスライスと内部のデータを共有していることです(後で詳しく説明します)。

開始インデックスや終了インデックスは省略できます。

  • `[:]`: 全範囲(元のスライスと同じ)
  • `[開始:]`: 開始インデックスから最後まで
  • `[:終了]`: 最初から終了インデックスの手前まで
package main

import "fmt"

func main() {
	original := []string{"a", "b", "c", "d", "e"}
	fmt.Println("元のスライス:", original)

	// インデックス1から3の手前まで (b, c)
	sub1 := original[1:3]
	fmt.Println("original[1:3]:", sub1)

	// インデックス2から最後まで (c, d, e)
	sub2 := original[2:]
	fmt.Println("original[2:] :", sub2)

	// 最初からインデックス4の手前まで (a, b, c, d)
	sub3 := original[:4]
	fmt.Println("original[:4] :", sub3)

	// 全部
	sub4 := original[:]
	fmt.Println("original[:]  :", sub4)

	// サブスライスの要素を変更すると…?
	sub1[0] = "B" // sub1の最初の要素 ('b') を 'B' に変更
	fmt.Println("sub1変更後のsub1:", sub1)
	fmt.Println("sub1変更後のoriginal:", original) // 元のスライスも変わってる!
}

実行結果

元のスライス: [a b c d e]
original[1:3]: [b c]
original[2:] : [c d e]
original[:4] : [a b c d]
original[:]  : [a b c d e]
sub1変更後のsub1: [B c]
sub1変更後のoriginal: [a B c d e]

最後の結果に注目!サブスライス(`sub1`)の要素を変更したら、元のスライス(`original`)の要素も変わってしまいましたね。

これは、サブスライスが元のスライスと内部のデータを共有している(同じ場所を見ている)からです。便利だけど、意図しない変更を引き起こす可能性もあるので注意が必要です。

スライスをコピーする`copy`関数の使い方

サブスライスのように元データと連動するのではなく、完全に独立したコピーを作りたい場合は、`copy`関数を使います。

`copy(コピー先スライス, コピー元スライス)` のように使います。

`copy`関数は、コピー元からコピー先に要素の値を単純にコピーします。
コピーされる要素の数は、コピー先とコピー元のスライスのうち、短い方の長さに合わせられます。戻り値は、実際にコピーされた要素数です。

コピー先のスライスは、あらかじめ`make`などで適切な長さ(コピーしたい要素数)で作っておく必要があります。

package main

import "fmt"

func main() {
	src := []int{10, 20, 30} // コピー元
	fmt.Println("コピー元:", src)

	// コピー先をmakeで作る (srcと同じ長さで)
	dest := make([]int, len(src))

	// copy関数でコピー実行
	numCopied := copy(dest, src)

	fmt.Println("コピー先:", dest)
	fmt.Println("コピーされた要素数:", numCopied)

	// コピー先の要素を変更しても、コピー元には影響しない!
	dest[0] = 100
	fmt.Println("コピー先変更後:", dest)
	fmt.Println("コピー元は変わらない:", src)

	fmt.Println("---")

	// コピー先の長さが短い場合
	shortDest := make([]int, 2) // 長さ2
	numCopied2 := copy(shortDest, src)
	fmt.Println("短いコピー先:", shortDest) // 最初の2要素だけコピーされる
	fmt.Println("コピーされた要素数:", numCopied2)
}

実行結果

コピー元: [10 20 30]
コピー先: [10 20 30]
コピーされた要素数: 3
コピー先変更後: [100 20 30]
コピー元は変わらない: [10 20 30]
---
短いコピー先: [10 20]
コピーされた要素数: 2

今度はコピー先の要素を変更しても、コピー元は影響を受けていませんね。
元のデータと切り離して扱いたい場合は`copy`関数を使いましょう。

Go言語のスライスを使う上での注意点

スライスはとても便利ですが、いくつか知っておくべき注意点があります。

特に「参照型」であることと、「`append`時の挙動」は、時々予期せぬ結果を招くことがあるので、頭に入れておきましょう。

スライスは参照型!挙動を理解しよう

スライスは参照型と呼ばれます。

これは、スライス変数自体が全てのデータを持っているわけではなく、実際のデータが格納されているメモリ上の場所(内部配列)を指し示している、という意味です。

どういうことかというと…

// スライスの内部イメージ図 (再掲)

+-----------+
| スライス変数 |
+-----------+
    |
    V
+-----------------+
| 内部配列へのポインタ | ----> [ データ1 | データ2 | ... ] (実際のデータはここにある)
+-----------------+
| 長さ            |
+-----------------+
| 容量            |
+-----------------+

スライスを別の変数に代入したり、関数の引数として渡したりすると、実際のデータそのものではなく、データへの参照(ポインタや長さ、容量の情報)がコピーされます。

結果として、複数のスライス変数が同じ内部配列を指し示す状況が生まれます。

package main

import "fmt"

func modifySlice(s []int) {
	fmt.Println("関数内の変更前:", s)
	s[0] = 999 // 関数内でスライスの要素を変更
	fmt.Println("関数内の変更後:", s)
}

func main() {
	original := []int{1, 2, 3}
	fmt.Println("元のスライス:", original)

	// 別の変数に代入 (参照がコピーされる)
	refCopy := original
	refCopy[1] = 222 // コピー先(のように見える)を変更

	// 元のスライスも変わってしまっている!
	fmt.Println("代入先変更後の元のスライス:", original)

	fmt.Println("---")

	// 関数にスライスを渡す
	modifySlice(original)

	// 関数内での変更が、呼び出し元のスライスにも影響している!
	fmt.Println("関数呼び出し後の元のスライス:", original)
}

実行結果

元のスライス: [1 2 3]
代入先変更後の元のスライス: [1 222 3]
---
関数内の変更前: [1 222 3]
関数内の変更後: [999 222 3]
関数呼び出し後の元のスライス: [999 222 3]

このように、ある場所でスライスを変更すると、同じ内部配列を参照している他のスライスにも影響が及びます。意図しない場所でデータが書き換わる可能性があるので、この挙動はしっかり覚えておきましょう。

`append`と容量の関係 - 思わぬ挙動に注意

`append`関数を使う際にも、参照と容量が絡んだ少しトリッキーな挙動があります。
さっき説明したように、`append`で容量が足りなくなると、新しい内部配列が確保されますよね。

もし、元のスライスからサブスライスを作り、そのサブスライスに対して`append`を行い、元の容量を超えなかった場合はどうなるでしょう?

答えは、元のスライスの内部配列の未使用領域にデータが書き込まれます。つまり、元のスライスにも影響が出る可能性があります。

さらに、サブスライスに対して`append`を行い、容量を超えて新しい内部配列が確保された場合、`append`後のサブスライスは新しい配列を指すようになり、元のスライスとは完全に別のものになります。

言葉だとややこしいので、コードで見てみましょう。

package main

import "fmt"

func main() {
	// 容量5のスライス
	original := make([]int, 3, 5) // [0 0 0], len=3, cap=5
	original[0], original[1], original[2] = 1, 2, 3 // [1 2 3]
	fmt.Println("元のスライス(original):", original, "len:", len(original), "cap:", cap(original))

	// originalからサブスライスを作成
	sub := original[1:3] // [2 3], len=2, cap=4 (元の容量から開始位置を引いた値になる)
	fmt.Println("サブスライス(sub):", sub, "len:", len(sub), "cap:", cap(sub))

	fmt.Println("--- subにappend (容量内) ---")
	// subに要素を1つappend (subの容量4なので、元の内部配列に書き込まれる)
	sub = append(sub, 4)
	fmt.Println("append後のsub:", sub, "len:", len(sub), "cap:", cap(sub))
	// originalも影響を受けている! (original[3]の位置に4が書き込まれた)
	fmt.Println("append後のoriginal:", original, "len:", len(original), "cap:", cap(original))

	fmt.Println("--- subにさらにappend (容量を超える) ---")
	// subにさらに要素を2つappend (subの容量4を超えるので、新しい配列が確保される)
	sub = append(sub, 5, 6)
	fmt.Println("再append後のsub:", sub, "len:", len(sub), "cap:", cap(sub)) // 新しい配列を指す
	// 今度はoriginalには影響なし (subは別の配列を見ているから)
	fmt.Println("再append後のoriginal:", original, "len:", len(original), "cap:", cap(original))

	// subの要素を変更しても、もうoriginalには影響しない
	sub[0] = 99
	fmt.Println("最後のsub:", sub)
	fmt.Println("最後のoriginal:", original)

}

実行結果

元のスライス(original): [1 2 3] len: 3 cap: 5
サブスライス(sub): [2 3] len: 2 cap: 4
--- subにappend (容量内) ---
append後のsub: [2 3 4] len: 3 cap: 4
append後のoriginal: [1 2 3] len: 3 cap: 5  // おっと、見た目変わらない?(※実はoriginal[3]に4が入っている。表示はlen=3まで)
--- subにさらにappend (容量を超える) ---
再append後のsub: [2 3 4 5 6] len: 5 cap: 8 // 容量が増え、新しい配列になった
再append後のoriginal: [1 2 3] len: 3 cap: 5  // こちらは元のまま
最後のsub: [99 3 4 5 6]
最後のoriginal: [1 2 3]

※ 実行結果の `append後のoriginal` が `[1 2 3]` のままに見えるのは、`original` の長さ `len` が 3 のままだからです。

内部配列の4番目の要素(`original[3]` に相当する場所)には、`sub` への最初の `append` によって `4` が書き込まれています。もし `original = original[:4]` のように長さを伸ばせば `[1 2 3 4]` と表示されます。

`append`が常に安全に元のスライスと独立するわけではない、という点は、少し複雑ですが重要なポイントです。混乱しそうなときは、`copy`関数を使って完全に別のスライスを作ってから操作するのが安全策かもしれませんね。

【まとめ】Go言語のスライスを使いこなそう!

Go言語のスライスについて、基本的なところから使い方、注意点まで見てきました。

最後に、今回のポイントをまとめておきましょう。

  • スライスは長さが可変なデータ列で、Goで超よく使う。
  • 作り方にはリテラル `[]T{...}` や `make([]T, len, cap)` がある。
  • `len()` で長さ、`cap()` で容量がわかる。
  • 要素追加は `append(slice, element...)`。新しいスライスが返る。容量が足りないと内部配列が再確保される。
  • 部分的な取り出しは `slice[start:end]`。元データと参照を共有する。
  • 完全なコピーは `copy(dest, src)` を使う。
  • スライスは参照型。複数の変数が同じデータを指すことがあり、変更が他に影響する場合がある。
  • `append`の挙動、特にサブスライスへの `append` は容量との関係で少し注意が必要。

スライスは、Go言語の柔軟性とパワーを支える機能の一つです。
最初は少し戸惑うかもしれませんが、使っていくうちにその便利さが実感できるはず。

この記事が、Go言語学習のサポートになれば嬉しいです。
自信を持って、どんどんコードを書いてスライスを使いこなしていってくださいね!

【関連記事】 Go言語とは?特徴・できること・将来性

このブログを検索

  • ()

自己紹介

自分の写真
リモートワークでエンジニア兼Webディレクターとして活動しています。プログラミングやAIなど、日々の業務や学びの中で得た知識や気づきをわかりやすく発信し、これからITスキルを身につけたい人にも役立つ情報をお届けします。 note → https://note.com/yurufuri X → https://x.com/mnao111

QooQ