この記事はマネーフォワードアドベントカレンダー2021🎄の22日目の記事です。
経理財務プロダクト本部でソフトウェアエンジニアをしている鈴木です。 開発を行っていてふとGo言語でDeep Copyをやりたいと考え、どのような実現方法があるか調査しました。
Deep Copy
Deep Copyとはオブジェクトを別の実体としてコピーすることです。
詳しくはGoogle検索してもらえればわかりやすい例がたくさん出てきます。
Deep Copyを行うためのハードルとしてGo言語ではポインタ型やSlice、Map型の存在があります。
これらの型は値としてメモリアドレス値を保持しており、代入演算子=
にてコピーを行ってもコピーされるものはメモリアドレス値であるため、同じ実体を指し示してしまいます。
これらの型に対して特別な操作でコピーする必要があるため単純な代入演算子=
だけではディープコピーが実現できません。
Go言語でDeep Copy
Go言語でポインタ型やslice, mapのディープコピーを行うためにはGo言語では代入=
ではなく、それらの型に合わせた操作を行う必要があります。
type Person struct { Name string Age int } var a *Person = &Person{Name: "A", Age: 10} var b *Person = &Person{} *b = *a // ポインタが指す実体をコピーする b.Name = "B" // BのNameを変更する fmt.Printf("address: %p, A: %#v\n", a, a) fmt.Printf("address: %p, B: %#v\n", b, b) // 結果 AとBは別のメモリアドレスを持つ address: 0xc00000c5a0, A: &main.Person{Name:"A", Age:10} address: 0xc00000c5b8, B: &main.Person{Name:"B", Age:10} // BだけNameが変更される
as := []int{1, 2, 3, 4, 5} bs := make([]int, len(as)) // 少なくともas以上の容量を持たなくてはコピーできない copy(bs, as) bs = append(bs, 6) // bsのみappendする fmt.Printf("address: %p, A: %#v\n", as, as) fmt.Printf("address: %p, B: %#v\n", bs, bs) // 結果 address: 0xc0000ba030, A: []int{1, 2, 3, 4, 5} address: 0xc0000e8000, B: []int{1, 2, 3, 4, 5, 6} // Bだけがappendされている
DeepCopyライブラリ
このようにポインタ型やslice, mapに対してDeepCopyを行うためには通常の代入とは違う操作が必要となるわけですが、これらを複数持つような複雑な構造体を定義すると、そのDeepCopyメソッドを手作業で記述することはやや骨が折れます。 そこでGo言語ではいくつかのDeepCopyライブラリが有志によって作成されています。
Go言語でDeepCopyを実現する方法の一例
- シリアライザを使って実体をbyte列に変換し、再び別の実体としてデシリアライズする。
- リフレクションを使ってコピーする
- ライブラリにてDeep Copyメソッドを自動生成する
- (1つ1つ手作業でコピーする)
4はライブラリを用いたものではないため括弧付きにしています。 それぞれの方法について解説します。
まず以下のような構造体を用意します。
type Member struct { Name string Age int secret int Family []string Job *Job } type Job struct { Name string Salary int } // 構造体のポインタに実体を作成 var member1 *Member = &Member{ Name: "MF1", Age: 30, secret: 100, Family: []string{ "MF2", "MF3", }, Job: &Job{ Name: "Software Engineer", Salary: 500, }, }
シリアライザを利用したDeep Copy
以下のようなメソッドを用意します。 シリアライザとしてJSONエンコーダーを使うかGobエンコーダーを使うかの違いなので結果は同じです。
func deepcopyJson(src interface{}, dst interface{}) (err error) { b, err := json.Marshal(src) if err != nil { return err } err = json.Unmarshal(b, dst) if err != nil { return err } return nil } func deepcopyGob(src interface{}, dst interface{}) (err error) { b := new(bytes.Buffer) enc := gob.NewEncoder(b) err = enc.Encode(src) if err != nil { return err } dec := gob.NewDecoder(b) err = dec.Decode(dst) if err != nil { return err } return nil } // DeepCopyテスト func test2(member1 *Member) { member2 := &Member{} deepcopyJson(member1, member2) member2.Age = 35 member2.Family = append(member1.Family, "MF4") member2.Job.Salary = 700 fmt.Printf("Member1: %#v, Job1:%#v\n", member1, member1.Job) fmt.Printf("Member2: %#v, Job1:%#v\n", member2, member2.Job) } // 結果 プライベートフィールド以外はDeepCopyされています Member1: &main.Member{Name:"MF1", Age:30, secret:100, Family:[]string{"MF2", "MF3"}, Job:(*main.Job)(0xc0000ae570)}, Job1:&main.Job{Name:"Software Engineer", Salary:500} Member2: &main.Member{Name:"MF1", Age:35, secret:0, Family:[]string{"MF2", "MF3", "MF4"}, Job:(*main.Job)(0xc0000ae6a8)}, Job1:&main.Job{Name:"Software Engineer", Salary:700}
リフレクションを用いたディープコピー
jinzhu/copierというライブラリを使わせていただきました。 仕組みとしてはリフレクションを用いて構造体が持つフィールドを解析し、コピーを行っているようです。
$ go get -u github.com/jinzhu/copier
// DeepCopyテスト func test3(member1 *Member) { member2 := &Member{} copier.CopyWithOption(member2, member1, copier.Option{ IgnoreEmpty: false, DeepCopy: true, }) member2.Age = 35 member2.Family = append(member1.Family, "MF4") member2.Job.Salary = 700 fmt.Printf("Member1: %#v, Job1:%#v\n", member1, member1.Job) fmt.Printf("Member2: %#v, Job1:%#v\n", member2, member2.Job) } // 結果 DeepCopyされていますがプライベートフィールドはコピーされないようです Member1: &main.Member{Name:"MF1", Age:30, secret:100, Family:[]string{"MF2", "MF3"}, Job:(*main.Job)(0xc000124570)}, Job1:&main.Job{Name:"Software Engineer", Salary:500} Member2: &main.Member{Name:"MF1", Age:35, secret:0, Family:[]string{"MF2", "MF3", "MF4"}, Job:(*main.Job)(0xc0001245d0)}, Job1:&main.Job{Name:"Software Engineer", Salary:700}
ライブラリにてDeep Copyメソッドを自動生成する
globusdigital/deep-copyというライブラリを使わせていただきました。
deep-copy
コマンドにてそれぞれの構造体に合わせたDeepCopyメソッドを自動生成してくれます。
$ go get github.com/globusdigital/deep-copy
$ deep-copy -o main_deep_copy.go --type Member .
// DeepCopyテスト func test4(member1 *Member) { member2 := member1.DeepCopy() member2.Age = 35 member2.Family = append(member1.Family, "MF4") member2.Job.Salary = 700 fmt.Printf("Member1: %#v, Job1:%#v\n", member1, member1.Job) fmt.Printf("Member2: %#v, Job1:%#v\n", member2, member2.Job) } // 結果 privateなフィールドもコピーされる Member1: &main.Member{Name:"MF1", Age:30, secret:100, Family:[]string{"MF2", "MF3"}, Job:(*main.Job)(0xc00000c588)}, Job1:&main.Job{Name:"Software Engineer", Salary:500} Member2: main.Member{Name:"MF1", Age:35, secret:100, Family:[]string{"MF2", "MF3", "MF4"}, Job:(*main.Job)(0xc00000c5a0)}, Job1:&main.Job{Name:"Software Engineer", Salary:700}
ベンチマーク
% go test -bench . goos: darwin goarch: amd64 pkg: test_deepcopy cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz BenchmarkMember_DeepCopyJson-16 495898 2352 ns/op BenchmarkMember_DeepCopyGob-16 55078 21616 ns/op BenchmarkMember_DeepCopyReflect-16 227006 5186 ns/op BenchmarkMember_DeepCopyGenerate-16 24410784 48.37 ns/op
ベンチマークを取った感じでは以下のような順位になりました。 1. 自動生成したコピーメソッドを用いたDeepCopy 48.37 ns/op 2. JSONシリアライザを用いたDeepCopy 2352 ns/op 3. リフレクションを用いたDeepCopy 5186 ns/op 4. Gobシリアライザを用いたDeepCopy 21616 ns/op
圧倒的に自動生成コピーメソッドが速く、JSONとReflectは2倍程度の差、Gobが最遅となりました。 Gobは謳い文句としてJSONよりも速いということでしたが今回は逆の結果になりました。 (Gobの使い方が間違っているような気もしています)
結論
利用の手軽さだけを考えるとシリアライザを使う方法かリフレクションを使う方法だと考えます。 Deep Copyメソッドをライブラリで自動生成するは他の方法よりも一手間かかることや、構造体のフィールドを変更したときは再び再生成し直さなければならない等、開発において考えなければならないことが増えてしまいます。 その一方で速度面では自動生成したコピーメソッドが圧倒的に速く、DeepCopyを速度が重視される場面で用いるならばこのライブラリを用いるか、手作業でディープコピーメソッドを書くほうが良いと思います。 またシリアライザやリフレクションを用いる方法ではプライベートフィールドがコピーされなかったためその点も注意する必要があります。 自分が行いたいDeep Copyが何を重視しているか、速度、手軽さ、プライベートフィールドのコピーの観点で考えてみて最適な選択を行うのが良いと思います。
私がDeepCopyしたいと考えた理由と用いた方法
今回、私はテストに用いる複雑な構造体のDeep Copyを行いたいと考えました。 テストのみに用いるため速度は重視しておらず、手軽に使いたいことと、全てがパブリックフィールドだったためJSONシリアライザを用いる方法で実装しました。
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』