Go言語のスライスを引数に渡す際の注意点!値渡しと参照渡しをわかりやすく解説
生徒
「先生、Go言語でスライスを関数に渡すとき、値渡しとか参照渡しとか聞くんですが、どういう意味ですか?」
先生
「いい質問です。プログラミングでの値渡しと参照渡しは少し難しい言葉ですが、簡単に言うと『関数にデータを渡すとき、コピーして渡すのか、それとも元のデータを直接渡すのか』という違いです。」
生徒
「なるほど。でもスライスはどうなるんですか?」
先生
「それが少し特殊で、スライスは『見た目は値渡しだけど、中身は参照渡しに近い』という特徴があります。これから詳しく説明しますね。」
1. 値渡しと参照渡しって何?
まず、「関数に渡したあと、元の変数が変わるのか?」という視点で考えると分かりやすいです。Go言語では基本は値渡しですが、ポインタ(住所のメモ)を使えば参照渡し風の振る舞いを実現できます。
- 値渡し(Call by Value):データのコピーを渡す方法。関数内で変更しても元の変数は変わりません。
- 参照渡し(Call by Reference):データが置いてある場所(アドレス)を渡す方法。関数内の変更が元の変数に反映されます。
イメージで言うと、値渡しは「書類のコピーを手渡す」、参照渡しは「本棚の場所メモを渡す」感じです。コピーを書き換えても原本は無傷、場所メモなら原本そのものを書き換えられます。
超シンプルな例(整数で体験)
package main
import "fmt"
// 値渡し:nのコピーだけが2倍になる
func doubleByValue(n int) {
n = n * 2
}
// 参照渡し風:nの住所(*int)を受け取り、元の値を2倍にする
func doubleByPointer(n *int) {
*n = *n * 2
}
func main() {
x := 10
doubleByValue(x)
fmt.Println("値渡しの後:", x) // 10 のまま
doubleByPointer(&x)
fmt.Println("参照渡し風の後:", x) // 20 に変わる
}
実行すると、値渡しの後はxがそのまま(コピーだけ変更されたため)、参照渡し風の後はxが変化します。これが「コピーを触るか、原本を触るか」の違いです。
2. Go言語は基本的に値渡し
Go言語では、関数に渡す引数は基本的に値渡しです。つまり、関数に渡すときにコピーが作られます。
ただし、コピーの大きさはデータの種類によって異なります。大きいデータはコピーに時間がかかります。
3. スライスは「構造体」のコピーが渡される
スライスは内部的には3つの情報を持つ構造体です。
- 配列の「先頭アドレス」
- スライスの「長さ」
- スライスの「容量」
関数にスライスを渡すときは、この構造体全体のコピーが渡されます。
ここでポイントは、コピーされるのは構造体(3つの情報)だけで、中身の配列データ自体はコピーされないということです。
4. そのため関数内でスライスの中身を変更すると元も変わる
構造体の中の「配列の先頭アドレス」は同じなので、関数内でスライスの中身(配列要素)を変更すると元の配列データも変わります。
つまり、スライスの中身は参照渡しに近い動きになります。
ただし、スライス自体の長さや容量を変更しても、コピーした構造体だけが変わるので、元のスライスの長さや容量は変わりません。
5. 実際にコードで確認しよう
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 100 // スライスの中身を変更
s = append(s, 200) // スライスの長さを変更(コピーにのみ影響)
fmt.Println("関数内:", s)
}
func main() {
nums := []int{1, 2, 3}
modifySlice(nums)
fmt.Println("関数外:", nums)
}
このコードの実行結果は次のようになります。
関数内: [100 2 3 200]
関数外: [100 2 3]
解説すると、関数内でスライスの中身(1番目の要素)が変更され、元のスライスにも反映されています。
しかし、appendでスライスを拡張した操作は関数内のコピーだけに影響し、元のスライスには反映されていません。
6. スライスを関数内で完全に変更したい場合はポインタを使う
もし関数内でスライス自体(長さや容量)も元の変数に反映させたい場合は、スライスのポインタ(*[]int)を渡す方法があります。
func modifySlicePointer(s *[]int) {
*s = append(*s, 300)
fmt.Println("関数内:", *s)
}
func main() {
nums := []int{1, 2, 3}
modifySlicePointer(&nums)
fmt.Println("関数外:", nums)
}
この場合、関数内でスライスの長さを変える操作も、元の変数に反映されます。
7. スライスの動きを理解すれば思い通りに扱える
Go言語のスライスを関数に渡すときは、構造体のコピーが渡されますが、中身の配列データは共有されます。
そのため、関数内でスライスの中身を変更すると元のデータも変わりますが、スライスの長さや容量の変更はコピーのみに影響します。
スライス自体を関数内で完全に変更したい場合は、ポインタを使って渡す方法を選びましょう。
このしくみを理解すると、Go言語で安全かつ効率的にスライスを扱えるようになります。
まとめ
Go言語でスライスを関数に渡すとき、見た目は値渡しでも中身の配列は共有される仕組みがあるため、少し混乱しやすいところです。今回の内容を振り返ると、スライスは「配列の先頭アドレス」「長さ」「容量」を持つ特別な構造体で、その構造体だけがコピーされ、実際の配列データは共有されます。つまり、関数内で値を変更したとき、元のデータにも反映されるという特徴があるので、意図せずデータが変わってしまうときは注意が必要になります。 また、スライスの長さや容量を変更すると、コピー側だけが大きくなり、元のスライスには反映されません。このような動きを理解しておくと、データ処理や大規模な処理でも混乱せずに進めることができます。 実務やチームでの開発でも、スライスの動きは重要なポイントとしてよく話題に上がります。配列との違いや、なぜ参照のように動くのかをしっかり知っておくことで、予期しないバグの防止にもつながります。毎回新しい変数を作る必要があるのか、ポインタを使うべきかなど、状況に応じて選べるようになるとより柔軟です。 さらに、関数内でスライスそのものを更新する必要がある場合には、ポインタを使うという選択肢が生まれます。ポインタで渡すことによって、スライスの中身だけでなく長さや容量を増やした場合も、元の変数に反映できるので、処理が複雑な場面でも役立ちます。スライスは配列より扱いやすく、可変で使いやすいデータ構造なので、Go言語のコードでは頻繁に登場します。そのため、今回の内容をしっかり理解することは、Go言語を扱ううえでとても大切な土台になります。 実際のプログラム例を交えて振り返りながら、スライスがどのように動くのかを再確認してみましょう。
スライス操作のサンプルプログラム例
こちらは、スライスを受け取る関数と、変更結果を確認するためのプログラムです。関数内での変更がどこまで外側に影響するのかを比較できます。
package main
import "fmt"
func changeSlice(s []int) {
s[1] = 50
s = append(s, 999)
fmt.Println("関数内:", s)
}
func main() {
numbers := []int{10, 20, 30}
changeSlice(numbers)
fmt.Println("関数外:", numbers)
}
このプログラムでは、関数内で二番目の要素を変更しています。その変更は元のスライスにも反映されています。ところが、appendで値を追加した後の長さは、関数外のスライスには反映されません。スライスの中身は共有されるのに、長さは共有されない。この少し不思議な動きこそが、スライスを理解する大切なポイントになります。 実践的な場面でも、データの加工やフィルタリングでスライスはよく使用されるので、意図せず元のデータが書き変わらないように注意したり、逆に意図して変更を反映させたいときにはポインタを使ったりと、柔軟に使い分ける力が求められます。
ポインタで渡すサンプルコード
今度はスライスのポインタを渡すパターンです。関数内でスライスに要素を追加して、元の変数にも反映される様子を確認できます。
func changePointerSlice(s *[]int) {
*s = append(*s, 777, 888)
fmt.Println("関数内:", *s)
}
func main() {
data := []int{5, 6, 7}
changePointerSlice(&data)
fmt.Println("関数外:", data)
}
この例では、appendの結果がそのまま元のスライスに反映されます。ポインタを使うことで、関数の外側にあるスライスをそのまま操作できるため、データ処理全体を見渡しながら安心して書き進めることができます。特に大量のデータを扱う場面や、複数の関数で同じスライスを扱う場面では便利です。
生徒
「スライスって値渡しなのに中身だけ変わるって不思議でした。でも構造体がコピーされるって考えたら納得できました。」
先生
「そうですね。中身の配列は同じ場所を見ているので、中身だけ書き変わるという特徴があります。実際に触りながら覚えると理解が深まりやすいですよ。」
生徒
「ポインタで渡すとスライスそのものも変わるんですね。状況によって使い分けるのが大切なんだとわかりました。」
先生
「その通りです。スライスは便利なので慣れるほど扱いやすくなりますよ。」