
あれ?スライスを普段通りコピーして値を更新したら、コピー元の値も変わってしまったよ…

スライスなどの参照型のコピーは注意が必要ですよ!
基本型(int型やstring型など)の変数をコピーするのとは違い、参照型(スライスやmap)の変数のコピーは基本型のコピー方法とは違います。
今回は、基本型の変数のコピーを復習し、参照型(特にスライス)のコピーについてお伝えしていきます。

スライスのコピーについて知りたい方は、ここからジャンプ!
基本型の変数をコピーしてみる
スライスのコピー操作に入る前に、基本型(今回はint型とint型の配列)のコピーを復習します。
そして、コピー先とコピー元の値の変化も確認していきましょう。
int型の変数をコピーして動きを確認する
// int型の変数をコピー
package main
import "fmt"
func main() {
// int型の変数を宣言
var a int = 10
// 変数aをコピー
a2 := a
// それぞれの値を確認
fmt.Println("変数aは", a, "です。")
fmt.Println("変数a2は", a2, "です。")
// 変数a2の値を書き換える
a2 = 1000
// それぞれの値を確認
fmt.Println("変数aは", a, "です。")
fmt.Println("変数a2は", a2, "です。")
}
// 出力結果
$ go run main.go
変数aは 10 です。
変数a2は 10 です。
変数aは 10 です。
変数a2は 1000 です。
int型の値が入った 変数a
をコピーされた 変数a2
のそれぞれ値が 10
代入されています。
そして、変数a2
の値を 1000
に変更しても、変数a
の値には影響していないことが確認できます。
int型の配列をコピーして動きを確認する
では、配列ではどうでしょうか。
// int型の配列をコピー
package main
import "fmt"
func main() {
// int型の配列を宣言
var b [3]int = [3]int{1, 2, 3}
// 配列bをコピー
b2 := b
// それぞれの値を確認
fmt.Println("配列bは", b, "です。")
fmt.Println("配列b2は", b2, "です。")
// 変数a2の値を書き換える
b2[0] = 1000
// それぞれの値を確認
fmt.Println("配列bは", b, "です。")
fmt.Println("配列b2は", b2, "です。")
}
// 出力結果
$ go run main.go
配列bは [1 2 3] です。
配列b2は [1 2 3] です。
配列bは [1 2 3] です。
配列b2は [1000 2 3] です。
配列に関しても、配列b2[0]
の値を 1000
に変更しても、配列b
には影響していないことが確認できます。
これは、基本型は値渡しで値をメモリ上にコピーしているからです。
スライスをコピーする
スライスのコピーについて、ゆっくりとお伝えしていきます。
スライスを基本型と同じようにコピーしてみる
スライスを基本型と同様にコピーしたら、どういう動きになるか確認してみましょう。
// スライスを基本型と同じようにコピー
package main
import "fmt"
func main() {
// スライスを宣言
sl := []int{100, 200}
// sl2にslをコピー
sl2 := sl
// sl2[0]の値を変更する
sl2[0] = 1000
// それぞれの値を確認
fmt.Println("sl =", sl)
fmt.Println("sl2 = ", sl2)
}
// 出力結果
$ go run main.go
sl = [1000 200]
sl2 = [1000 200]

あれ? sl2[0]
に値を変えたのに、sl[0]
も変わってますね。

スライスは参照型なので、アドレスの値をコピーしているからです。
参照型では値をコピーしているのではなく、アドレスをコピーしている(参照渡し)のです。
では、sl
と sl2
のアドレスを確認してみましょう。
// スライスを基本型と同じようにコピー
package main
import "fmt"
func main() {
// スライスを宣言
sl := []int{100, 200}
// sl2にslをコピー
sl2 := sl
// sl2[0]の値を変更する
sl2[0] = 1000
// それぞれの値を確認
fmt.Println("sl =", sl)
fmt.Println("sl2 = ", sl2)
// アドレスを確認
fmt.Printf("sl = %p\n", sl)
fmt.Printf("sl2 = %p\n", sl2)
}
// 出力結果
$ go run main.go
sl = [1000 200]
sl2 = [1000 200]
sl = 0xc000014100
sl2 = 0xc000014100
sl
と sl2
のアドレスが一緒です。
スライスを基本型のようにコピーすると値がコピーされるのではなく、アドレスがコピーされるので sl2[0] = 1000
のような操作をすると直接アドレスの値が変更され sl[0]
にも影響が出ます。
copy関数を使用してスライスをコピーする
スライスをコピーするには、make関数
と copy関数
を使用します。
copy関数は下記のように記述します。
では、実際にプログラムで確認していきましょう。
// copy関数を使用してスライスをコピー
package main
import "fmt"
func main() {
// スライスを宣言する
sl3 := []int{1, 2, 3, 4, 5}
// sl3を出力する
fmt.Println(sl3)
// スライスのメモリ領域を確保する
sl4 := make([]int, 5, 5)
// sl4の現状を出力する
fmt.Println(sl4)
// スライスはcopy関数を使用する
// 変数cはコピーに成功した要素数が格納される
c := copy(sl4, sl3)
fmt.Println(c, sl4)
// アドレスを確認
fmt.Printf("sl3 = %p\n", sl3)
fmt.Printf("sl4 = %p\n", sl4)
// sl4[0]の値を変更する
sl4[0] = 1000
// それぞれのスライスを出力
fmt.Println(sl3)
fmt.Println(sl4)
}
// 出力結果
$ go run main.go
[1 2 3 4 5]
[0 0 0 0 0] // メモリにスライスの領域を確保
5 [1 2 3 4 5] // コピーに成功した値とコピーされたスライス
sl3 = 0xc00001a2d0 // アドレスの違いを確認できる
sl4 = 0xc00001a300
[1 2 3 4 5]
[1000 2 3 4 5] // sl4[0]の値が変更されている
make関数
でスライスのメモリに領域を確保してから、コピー元の sl3
のスライスの要素が sl4
にきちんとコピーされ、アドレスの違いも確認できます。
そして、sl4[0]
の値を変更しても sl3
には影響していないことが確認できました。
要素数の違うスライスをコピーする
先程のスライスのコピーは、どちらも要素数が同じでした。
では、要素数の違うスライスをコピーすると、どういう動きになるのか確認してみましょう。
// 要素数の違うスライスをコピー
package main
import "fmt"
func main() {
// スライスを宣言する
sl5 := []int{10, 20, 30, 40, 50}
// sl5を出力する
fmt.Println(sl5)
// スライスのメモリ領域を確保する
sl6 := make([]int, 3, 5)
// sl6の現状を出力する
fmt.Println(sl6)
// スライスはcopy関数を使用する
// 変数cはコピーに成功した要素数が格納される
d := copy(sl6, sl5)
fmt.Println(d, sl6)
}
// 出力結果
$ go run main.go
[10 20 30 40 50]
[0 0 0]
3 [10 20 30]
make関数
で要素数を 3
で指定したので、スライスのコピーに成功したのは 3
でした。
この場合、左から(インデックス番号0から)3つの要素がコピーされます。
【まとめ】スライスをコピーする(copy関数)
今回は、スライスのコピーについてお伝えしてきました。
それでは、まとめにはいります。
以上です。
値渡しと参照渡しが理解できていれば、スライスのコピーに関しては問題なく理解できたと思います。
実際にプログラムのコードを書いて、いろんなパターンの動きを確かめてみましょう。
comment