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らしく感じます。
Discussion