技術メモ

技術メモ

ラフなメモ

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