markdown

はじめに (対象読者・この記事でわかること)

本記事は、Goで開発を行っているエンジニア(初心者から中級者)を対象としています。特に、関数やメソッドの引数として interface{} を受け取り、その内容を安全にコピーしたいと考えている方に向けて書かれています。この記事を読むと、以下のことが分かります。

  • interface{} が内部でどのように表現されているか
  • シャローコピーとディープコピーの違いと適切な選択基準
  • Reflection とジェネリクスを組み合わせた汎用的なコピー関数の実装方法
  • 実装時に陥りやすい落とし穴とその回避策

Goの型安全性を保ちつつ、柔軟にデータを複製するテクニックを身につけましょう。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Go言語の基本的な文法と型システム
  • interface{} の概念と型アサーション、タイプスイッチの使い方
  • reflect パッケージの基本的な使い方(reflect.Valuereflect.Type
  • Go 1.18 以降のジェネリクス(type parameters)に関する基礎知識

背景と概要:なぜinterfaceのコピーが必要なのか

interface{} は任意の型を受け取れる柔軟性がある反面、内部的には ポインタと型情報の組み合わせ で実装されています。関数に渡された interface{} をそのまま代入すると、実際には 参照(ポインタ) がコピーされるだけで、元のオブジェクトと同一のメモリ領域を指すことになります。これが原因で、コピー後に元データを変更すると、コピー側にも影響が及ぶ「シャローコピー」の問題が発生します。

多くのケースで求められるのは ディープコピー、すなわちオブジェクト全体を再帰的に複製し、独立したメモリ領域に格納することです。特に以下のようなシナリオでディープコピーは必須です。

  • API のリクエストハンドラで受け取ったデータを別スレッドに渡す場合
  • キャッシュに格納したオブジェクトが他箇所で変更されるリスクを排除したい場合
  • テストコードでオブジェクトの状態を比較したいが、元データが変わらないようにしたい場合

しかし、Go には標準で「任意の interface{} をディープコピーする」ユーティリティは存在しません。そのため、開発者は Reflectionジェネリクス を駆使して自前のコピー関数を実装する必要があります。本記事では、実装パターンを段階的に示し、実務で安全に利用できるコード例を提供します。

具体的な手順と実装方法

以下では、汎用的に利用できる CopyInterface 関数を作成する手順を解説します。大きく分けて 3 つのステップに分かれます。

ステップ1:コピー対象の具体型を取得する

interface{} が保持している具体型情報は reflect.TypeOfreflect.ValueOf で取得できます。まずはこの情報を元に、コピー先の新しいインスタンスを作ります。

Go
func copyValue(src reflect.Value) reflect.Value { // ポインタかどうかで分岐 if src.Kind() == reflect.Ptr { // nil ポインタはそのまま返す if src.IsNil() { return reflect.Zero(src.Type()) } // 新しいポインタを作成 dst := reflect.New(src.Type().Elem()) // 再帰的に中身をコピー dst.Elem().Set(copyValue(src.Elem())) return dst } // スライス、マップ、配列は要素ごとに再帰 switch src.Kind() { case reflect.Slice: if src.IsNil() { return reflect.Zero(src.Type()) } dst := reflect.MakeSlice(src.Type(), src.Len(), src.Cap()) for i := 0; i < src.Len(); i++ { dst.Index(i).Set(copyValue(src.Index(i))) } return dst case reflect.Map: if src.IsNil() { return reflect.Zero(src.Type()) } dst := reflect.MakeMapWithSize(src.Type(), src.Len()) for _, key := range src.MapKeys() { dst.SetMapIndex(key, copyValue(src.MapIndex(key))) } return dst case reflect.Struct: dst := reflect.New(src.Type()).Elem() for i := 0; i < src.NumField(); i++ { // エクスポートされていないフィールドはスキップ(安全のため) if src.Type().Field(i).PkgPath != "" { continue } dst.Field(i).Set(copyValue(src.Field(i))) } return dst default: // 基本型はそのままコピー return src } }

上記 copyValue は、ポインタ・スライス・マップ・構造体を再帰的に走査し、可能な限りディープコピーを行います。エクスポートされていないフィールドはコピー対象外 にすることで、panic: reflect: cannot set unexported field を防ぎます。

ステップ2:汎用的な CopyInterface 関数を定義する

次に、上記の copyValue を組み合わせて、任意の interface{} を受け取り、コピーした新しい interface{} を返す関数を作ります。

Go
// CopyInterface は任意の interface{} をディープコピーします。 // コピーに失敗した場合は panic します(呼び出し元で recover してください)。 func CopyInterface(src interface{}) interface{} { if src == nil { return nil } v := reflect.ValueOf(src) copied := copyValue(v) return copied.Interface() }

この関数は非常にシンプルですが、内部で行っている再帰コピーロジックにより、ほとんどの標準的なデータ構造を安全に複製できます。また、ジェネリクス版として以下のように書くことも可能です。

Go
// Copyは型パラメータ T を受け取り、T のディープコピーを返します。 func Copy[T any](src T) T { if any(src) == nil { var zero T return zero } v := reflect.ValueOf(src) copied := copyValue(v) return copied.Interface().(T) }

ステップ3:実装例とテストケース

以下は、実際に CopyInterface を利用したサンプルコードです。構造体・スライス・マップを組み合わせたケースでディープコピーが正しく動作することを確認します。

Go
package main import ( "fmt" "reflect" ) type Person struct { Name string Age int Tags []string Profile map[string]interface{} } func main() { original := Person{ Name: "Alice", Age: 30, Tags: []string{"golang", "dev"}, Profile: map[string]interface{}{ "city": "Tokyo", "active": true, }, } // interface 版コピー copiedIf := CopyInterface(original).(Person) // ジェネリクス版コピー copiedGen := Copy(original) // 変更しても元が変わらないことを確認 copiedIf.Tags[0] = "python" copiedIf.Profile["city"] = "Osaka" fmt.Printf("Original: %+v\n", original) fmt.Printf("CopiedIf: %+v\n", copiedIf) fmt.Printf("CopiedGen: %+v\n", copiedGen) }

実行結果の一部は次のようになります。

Original: {Name:Alice Age:30 Tags:[golang dev] Profile:map[active:true city:Tokyo]}
CopiedIf: {Name:Alice Age:30 Tags:[python dev] Profile:map[active:true city:Osaka]}
CopiedGen: {Name:Alice Age:30 Tags:[golang dev] Profile:map[active:true city:Tokyo]}

CopiedIf の内部を変更しても、original が影響を受けていないことが確認できます。ジェネリクス版 Copy も同様に機能します。

ハマった点やエラー解決

1. 非エクスポートフィールドのコピーで panic

reflect で構造体の非エクスポートフィールドを Set しようとすると、panic: reflect: cannot set unexported field が発生します。対策として、PkgPath が空でないフィールド(非エクスポート)をスキップするロジックを追加しました。

2. 循環参照(サイクル)による無限再帰

自己参照構造体や循環グラフをコピーしようとすると copyValue が無限に呼び出され、スタックオーバーフローになります。実務で循環参照が必要な場合は、マップで済んでいるオブジェクトをトラッキングし、既にコピー済みのポインタはそのまま再利用する実装(sync.Map でキャッシュ)を組み込む必要があります。今回の実装はシンプルさを優先し、循環参照はサポートしていません。

3. nil ポインタ・nil スライスの扱い

コピー対象が nil の場合、単に reflect.Zero を返すだけでは型情報が失われ、後続の型アサーションが失敗します。したがって、src.IsNil() を判定し、元の型情報を保ったまま nilreflect.Value を生成するようにしました。

解決策まとめ

課題 解決策
非エクスポートフィールドで panic PkgPath が空でないフィールドをスキップ
循環参照で無限再帰 コピー済みオブジェクトをマップで管理(高度な実装が必要)
nil 値の扱い reflect.ZeroIsNil を組み合わせて型情報を保持

まとめ

本記事では、Go における interface{} のディープコピー手法を体系的に解説しました。

  • interface{} は内部的にポインタと型情報の組み合わせであり、単純な代入はシャローコピーになる
  • reflect と再帰的なコピーロジックで、ポインタ・スライス・マップ・構造体を安全にディープコピーできる
  • ジェネリクスを併用すれば、型安全な汎用コピー関数 Copy[T any] を簡潔に提供できる

これらのテクニックを活用すれば、データの不意な共有によるバグを防ぎつつ、柔軟な API 設計が可能になります。今後は、循環参照を持つグラフ構造や、カスタムシリアライズ方式(JSON・MessagePack)との併用についても検討していく予定です。

参考資料