Goで構造体をDeepCopyする

公開:2020/09/26
更新:2020/09/26
8 min読了の目安(約5000字TECH技術記事

はじめに

Goで構造体のDeepCopyする方法の紹介です。Goのsliceやmapは値への参照を保持しているため、単純に値をコピーするだけではDeepCopyできないことはよく知られています。以下は間違った実装例です。

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4}
	t := s
	fmt.Printf("[s] address: %p, values: %v\n", s, s)
	fmt.Printf("[t] address: %p, values: %v\n", t, t)
	// Output:
	//[s] address: 0xc00009e140, values: [1 2 3 4]
	//[t] address: 0xc00009e140, values: [1 2 3 4]

	// 参照先のアドレスの値を変更
	s[0] = 999

	fmt.Printf("[s] address: %p, values: %v\n", s, s)
	fmt.Printf("[t] address: %p, values: %v\n", t, t)
	// Output:
	//[s] address: 0xc00009e140, values: [999 2 3 4]
	//[t] address: 0xc00009e140, values: [999 2 3 4]
}

sliceのDeepCopyは Go Slices: usage and internals にあるように組み込み関数である copy を用いるのが正しい実装になります。(アドレスは実行環境によって変わります)

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4}
	t := make([]int, len(s))
	copy(t, s)
	fmt.Printf("[s] address: %p, values: %v\n", s, s)
	fmt.Printf("[t] address: %p, values: %v\n", t, t)
	// Output:
	// [s] address: 0xc00000c3c0, values: [1 2 3 4]
	// [t] address: 0xc00000c3e0, values: [1 2 3 4]

	// 参照先のアドレスの値を変更
	s[0] = 999

	fmt.Printf("[s] address: %p, values: %v\n", s, s)
	fmt.Printf("[t] address: %p, values: %v\n", t, t)
	// Output:
	// [s] address: 0xc00000c3c0, values: [999 2 3 4]
	// [t] address: 0xc00000c3e0, values: [1 2 3 4]
}

構造体のDeepCopy

リフレクションを用いてGoでDeepCopyするライブラリは以下のようなライブラリがあります。

リフレクションを用いたDeepCopyは本記事では立ち入りませんので、紹介だけにとどめておきます。

go generateによるメソッド生成

本記事では go generate によって構造体のメソッドを生成してDeepCopyする方法を紹介します。例として以下のような myStruct をDeepCopyすることにします。

  • mystruct.go
type myStruct struct {
	A int
	B string
	C []string
	D map[string]string
	E map[string][]string
}

以下のライブラリを用いてDeepCopyするメソッドを生成します。

ライブラリをインストールします。

$ go get github.com/globusdigital/deep-copy

メソッドを生成します。

$ deep-copy -o mystruct_deepcopy.go --type myStruct .

するとコマンドで指定したファイルが生成されます。メソッドの実装は、もとの構造体の値をまずはそのままコピーしています。その後、sliceやmapのフィールドの値に関しては、make をして新しいメモリアドレスを確保し、組み込みのcopy関数を用いてDeepCopyしています。

  • mystruct_deepcopy.go
// generated by deep-copy -o mystruct_deepcopy.go --type myStruct .; DO NOT EDIT.

package main

// DeepCopy generates a deep copy of myStruct
func (o myStruct) DeepCopy() myStruct {
	var cp myStruct = o
	if o.C != nil {
		cp.C = make([]string, len(o.C))
		copy(cp.C, o.C)
	}
	if o.D != nil {
		cp.D = make(map[string]string, len(o.D))
		for k, v := range o.D {
			cp.D[k] = v
		}
	}
	if o.E != nil {
		cp.E = make(map[string][]string, len(o.E))
		for k, v := range o.E {
			var cpv []string
			if v != nil {
				cpv = make([]string, len(v))
				copy(cpv, v)
			}
			cp.E[k] = cpv
		}
	}
	return cp
}

テストして構造体を比較します。テストには google/go-cmp を利用します。

  • mystruct_test.go
package main

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

func Test_newMyStruct(t *testing.T) {
	src := &myStruct{
		A: 1,
		B: "test",
		C: []string{"A", "B", "C"},
		D: map[string]string{
			"A": "Apple",
		},
		E: map[string][]string{
			"fruits": {"Apple", "Banana"},
		},
	}
	tar := src.DeepCopy()

	if diff := cmp.Diff(*src, tar); diff != "" {
		t.Errorf("mystruct mismatch (-want +got):\n%s", diff)
	}

	src.A = 999
	src.B = "XXXXX"
	src.C[0] = "XXXXX"
	src.D["A"] = "XXXXX"
	src.E["fruits"] = []string{"XXXXX"}

	diff := cmp.Diff(*src, tar)
	if diff == "" {
		t.Errorf("expect different mystruct")
	}
	t.Logf("mystruct different (-src +tar):\n%s", diff)

	expect := myStruct{
		A: 1,
		B: "test",
		C: []string{"A", "B", "C"},
		D: map[string]string{
			"A": "Apple",
		},
		E: map[string][]string{
			"fruits": {"Apple", "Banana"},
		},
	}
	if diff := cmp.Diff(expect, tar); diff != "" {
		t.Errorf("mystruct mismatch (-want +got):\n%s", diff)
	}
}

以下のように想定どおりDeepCopyされていることが確認できました。

$ go test -v
=== RUN   Test_newMyStruct
    Test_newMyStruct: mystruct_test.go:37: mystruct different (-src +tar):
          main.myStruct{
        -       A: 999,
        +       A: 1,
        -       B: "XXXXX",
        +       B: "test",
                C: []string{
        -               "XXXXX",
        +               "A",
                        "B",
                        "C",
                },
        -       D: map[string]string{"A": "XXXXX"},
        +       D: map[string]string{"A": "Apple"},
                E: map[string][]string{
                        "fruits": {
        -                       "XXXXX",
        +                       "Apple",
        +                       "Banana",
                        },
                },
          }
--- PASS: Test_newMyStruct (0.00s)
PASS
ok      github.com/d-tsuji/go-sandbox/s 0.190s

まとめ

globusdigital/deep-copy のライブラリを用いて構造体にDeepCopy可能なメソッドを生成することで、構造体をDeepCopyする方法を紹介しました。go generateを用いてメソッドを生成する方法は Generating code で紹介されている stringer をgo generateするアプローチと同様です。リフレクションを用いて動的にDeepCopyする方法よりもGoらしく感じます。