Goで構造体をDeepCopyする
はじめに
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らしく感じます。