Go言語において、スライスは非常に強力かつ柔軟なデータ構造として日常的に利用されます。
しかし、スライスの「コピー」という操作には、初心者が陥りやすい罠がいくつか存在します。
スライスの内部構造を正しく理解せずにコピーを行うと、意図しないデータの書き換えやメモリ効率の低下を招く恐れがあります。
本記事では、Go言語でスライスを安全かつ効率的にコピーするための標準的な手法から、応用的なパターンまでを詳しく解説します。
開発現場で即戦力となる知識を身につけ、堅牢なGoプログラムを作成するための参考にしてください。
スライスのコピーにおける根本的な誤解
Go言語でスライスを扱う際、最も注意すべき点は「代入」と「コピー」の違いを明確に区別することです。
多くの初心者は、変数に別のスライスを代入するだけで複製が完了したと考えてしまいます。
しかし、Goのスライスは内部的に「ポインタ」「長さ(len)」「容量(cap)」の3つの要素を持つ構造体であることを忘れてはいけません。
代入演算子による浅いコピーの危険性
変数に対してスライスをそのまま代入した場合、それは「浅いコピー(Shallow Copy)」となります。
この操作では、スライスのヘッダー情報のみがコピーされ、背後にある実体データ(基底配列)は共有されたままになります。
以下のコード例で、代入がどのような挙動を示すかを確認してみましょう。
package main
import "fmt"
func main() {
// オリジナルのスライスを作成
original := []int{1, 2, 3, 4, 5}
// 代入によるコピーを試みる
copied := original
// コピーしたはずのスライスの要素を書き換える
copied[0] = 999
fmt.Println("Original:", original)
fmt.Println("Copied: ", copied)
}
Original: [999 2 3 4 5]
Copied: [999 2 3 4 5]
実行結果からわかる通り、copied の要素を変更すると 元の original の内容まで書き換わってしまいます。
これは、両方のスライスがメモリ上の同じ基底配列を参照しているために発生する現象です。
意図的に参照を共有する場合を除き、この挙動は予期せぬサイドエフェクトを引き起こす原因となります。
標準関数 copy() を使った安全なコピー
スライスの要素を物理的に別のメモリ領域へ複製するには、Go言語の組み込み関数である copy() を使用します。
copy() 関数を使用することで、元のスライスとは独立した新しいデータを作成することが可能です。
この関数の基本的なシグネチャは func copy(dst, src []Type) int となっています。
copy() 関数の基本的な使い方
copy() を利用する際は、まずコピー先となるスライスを適切なサイズで事前に作成しておく必要があります。
実際に copy() を使った安全な複製方法を見ていきましょう。
package main
import "fmt"
func main() {
src := []int{10, 20, 30, 40, 50}
// srcと同じ長さのスライスをmakeで作成する
dst := make([]int, len(src))
// copy関数で要素を転送する
count := copy(dst, src)
// コピー先のみを書き換える
dst[0] = 100
fmt.Printf("コピーされた要素数: %d\n", count)
fmt.Println("Original (src):", src)
fmt.Println("New Slice (dst):", dst)
}
コピーされた要素数: 5
Original (src): [10 20 30 40 50]
New Slice (dst): [100 20 30 40 50]
この例では、dst の値を変更しても src には影響が及んでいないことが確認できます。
これがGo言語における「深いコピー(Deep Copy)」の最も標準的な実装方法です。
【重要】コピー先の長さ(len)に依存する仕様
copy() 関数を使用する際に、最も間違いやすいポイントが「コピー先の長さ」に関する仕様です。
copy() は、「コピー先のスライスの長さ」と「コピー元のスライスの長さ」のうち、小さい方の数だけ要素をコピーします。
もしコピー先のスライスの長さが 0 であった場合、コピー処理は一切行われません。
package main
import "fmt"
func main() {
src := []int{1, 2, 3}
// 長さを指定せずにスライスを宣言(len=0)
var dst []int
// この場合、何もコピーされない
copy(dst, src)
fmt.Println("dst:", dst) // 結果は []
}
このミスを防ぐためには、必ず make で len(src) と同等の長さを確保するようにしてください。
容量(cap)ではなく、長さ(len)が基準となる点に注意が必要です。
append() 関数を利用したスライスのコピー
Go 1.22 以降やモダンなGo開発では、append() 関数を活用した短い記述でスライスをコピーする手法も好まれます。
この方法は make と copy を分ける必要がないため、コードを簡潔に保つことができます。
イディオムとしての append 活用法
以下の記法は、Goのイディオムとして非常によく使われます。
dst := append([]int(nil), src...)
この一行のコードは、以下の処理を同時に行っています。
[]int(nil)により、nilスライスをベースとして用意する。src...(可変長引数展開) により、srcの全要素をappendする。appendの内部処理により、適切なメモリが自動的に割り当てられる。
copy() 関数を使う場合と比較して、「コピー先の長さを事前に計算して定義する手間」が省けるのが大きな利点です。
どちらの手法を選ぶべきか
使い分けの基準については、以下の表を参考にしてください。
| 手法 | メリット | デメリット / 注意点 |
|---|---|---|
copy() 関数 | 意図が明確で、既存のスライスの一部への上書きも可能。 | 事前に make で長さを確保する必要がある。 |
append() イディオム | 1行で記述でき、コードがシンプル。 | 新規スライス作成専用であり、既存スライスへのコピーには不向き。 |
多次元スライスのコピーにおける注意点
スライスがスライスを保持している「多次元スライス」の場合、copy() 関数だけでは不十分です。
copy() は第一階層の要素(つまり内部スライスのヘッダー)のみをコピーするため、第二階層のスライスは参照を共有したままになります。
多次元スライスの深いコピーの実装
完全な独立性を確保するためには、ループを使用して各要素を個別にコピーする必要があります。
package main
import "fmt"
func main() {
src := [][]int{{1, 2}, {3, 4}}
// コピー先を確保
dst := make([][]int, len(src))
for i := range src {
// 各要素(スライス)に対して再度コピーを行う
dst[i] = make([]int, len(src[i]))
copy(dst[i], src[i])
}
dst[0][0] = 999
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
このように、データ構造が複雑な場合は、どこまでが共有され、どこからが独立しているかを常に意識する必要があります。
パフォーマンスに関する考察
大量のデータを扱う際、コピーのコストは無視できません。
Goの copy() 関数は、アーキテクチャごとに最適化されたメモリコピー(memmove)を実行するため、非常に高速です。
事前割り当て(Pre-allocation)の重要性
append によるコピーは便利ですが、大量の要素を追加する際に内部で何度もメモリ再割り当てが発生する可能性があります。
パフォーマンスを極限まで追求する場合は、あらかじめ make で容量を指定した上で copy() を使うのが最適です。
特に数万件規模のデータを扱うループ処理内では、この僅かな差が実行時間に大きな影響を及ぼします。
よくあるミスとデバッグ方法
スライスのコピーに関連するバグは、一見すると正常に動作しているように見えるため発見が遅れがちです。
よくあるミスとして、スライスの「スライス式」を用いた際の部分的な共有が挙げられます。
sub := original[1:3] と記述した場合、sub は original の基底配列のサブセットを参照しているだけです。
この状態で sub を書き換えると original も変わってしまいます。
もし部分的に抽出したデータを独立させたいのであれば、抽出後に必ず copy() を行う癖をつけましょう。
まとめ
Go言語のスライスを安全にコピーするためには、copy() 関数と append() イディオムの使い分けを理解することが不可欠です。
単なる代入は「参照の共有」であり、実データの複製ではないことを常に意識してください。
特に copy() を使用する際は、コピー先のスライスの長さ(len)が正しく確保されているかを確認することが最も重要です。
また、多次元スライスの場合は再帰的なコピーが必要になる点も忘れてはいけません。
これらのルールを徹底することで、データ競合や意図しない破壊的変更を防ぎ、信頼性の高いアプリケーションを構築できるようになります。
スライスの特性を深く理解し、状況に応じた最適なコピー手法を選択してください。
