いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-89 (go-cmp の使い方メモ)(比較処理用のライブラリ, Comparer)

概要

比較処理を作っている際に reflect.DeepEqual 使っていたんですが、この子、プライベートなフィールドまで比較対象にしてくれるんですね。。

ちょっと、それは必要なかったので、なんかライブラリ無いかなって探してみたら、go-cmp ってのを発見。

有名みたいですね。

github.com

特徴としては

  • diffコマンドの結果みたいなユーザフレンドリーな出力をしてくれる
  • 型にEqualメソッドを定義しておくと、それを呼んでくれる
  • プライベートなフィールドを比較対象にするかどうかを選べる
  • 無視するフィールドも選べる

とかがあるので、いい感じ。

ちょっと、自分用にサンプルつくったので、以下にメモメモ。

サンプル

package gocmp

import (
    "strings"

    "github.com/devlights/gomy/output"
    "github.com/google/go-cmp/cmp"
    "github.com/google/go-cmp/cmp/cmpopts"
)

type (
    A struct {
        PublicField string
    }

    B struct {
        A
        privateField string
    }

    C A
)

// Equal -- 自身と指定された値が等しいかどうかを返します
//
// 等しい場合は true, それ以外は false です.
func (c C) Equal(o C) bool {
    upper1 := strings.ToUpper(c.PublicField)
    upper2 := strings.ToUpper(o.PublicField)

    return upper1 == upper2
}

// Basic -- go-cmp の基本パターンについてのサンプルです.
func Basic() error {
    // ---------------------------------------------------------------------------------
    // go-cmp は、比較処理用のライブラリ
    //
    // 標準の比較機能、つまり、reflect.DeepEqual よりもフレキシブルな比較処理を作ることが出来る
    //
    // 以下でインストールする。
    //   go get -u github.com/google/go-cmp/cmp
    //
    // よく利用する機能は、 cmp.Diff() と cmp.Equal()
    //
    // 基本的な機能として、対象となる型に Equal メソッドが定義されていれば
    // それを使って比較をしてくれる。
    //
    // ---
    // 参考
    //   - https://qiita.com/iszk/items/e799ec4d6f1f5eece706
    //   - https://pkg.go.dev/mod/github.com/google/go-cmp
    // ---------------------------------------------------------------------------------

    // ---------------------------------------------------------------------------------
    // cmp.Diff()
    //   diff コマンドのような比較結果を出力してくれる
    a1 := A{PublicField: "hello"}
    a2 := A{PublicField: "HeLLo"}

    if diff := cmp.Diff(a1, a2); diff != "" {
        // 差異あり
        output.Stdoutl("Diff(a1, a2)", diff)
    }

    // ---------------------------------------------------------------------------------
    // デフォルトでは プライベートなフィールド があると panic する
    // そのまま実行すると以下のメッセージが出る
    //   panic: cannot handle unexported field at {gocmp.B}.privateField:
    //   "github.com/devlights/try-golang/examples/advanced/gocmp".B
    b1 := B{
        A:            a1,
        privateField: "hello",
    }
    b2 := B{
        A:            a2,
        privateField: "world",
    }

    // ---------------------------------------------------------------------------------
    // プライベートなフィールドを比較対象に含めるには、オプションで設定する
    // cmp.AllowUnexported は内部で reflect.TypeOf して型情報を取得してマッピングを生成している
    opt := cmp.AllowUnexported(b1)
    if diff := cmp.Diff(b1, b2, opt); diff != "" {
        // 差異あり
        output.Stdoutl("Diff(b1, b2, cmp.AllowUnexported)", diff)
    }

    // プライベートなフィールドを無視するのも、オプションで設定する
    // cmpopts.IgnoreUnexported は内部で reflect.TypeOf して型情報を取得してマッピングを生成している
    opt = cmpopts.IgnoreUnexported(b1)
    if diff := cmp.Diff(b1, b2, opt); diff != "" {
        // 差異あり
        output.Stdoutl("Diff(b1, b2, cmpopts.IgnoreUnexported)", diff)
    }

    // ---------------------------------------------------------------------------------
    // Equal メソッドが定義されている場合、それを使ってくれる
    c1 := C{
        PublicField: "hello",
    }
    c2 := C{
        PublicField: "HeLLo",
    }

    if diff := cmp.Diff(c1, c2); diff != "" {
        // 差異あり
        output.Stdoutl("Diff(c1, c2)", diff)
    } else {
        output.Stdoutl("Diff(c1, c2)", "差異なし")
    }

    // ---------------------------------------------------------------------------------
    // cmp.Equal() は、Diff と同じように比較してくれるが 戻り値が bool となる
    output.Stdoutl("Equal(a1, a2)", cmp.Equal(a1, a2))
    output.Stdoutl("Equal(c1, c2)", cmp.Equal(c1, c2))

    // ---------------------------------------------------------------------------------
    // cmp.Comparer(f interface{}) Option を利用して、専用の Comparer を作って
    // 比較することも出来る
    //
    // cmp.Comparer() の引数は interface{} となっているが
    //   func (T,T) bool
    // を渡さないといけない。
    //  > The comparer f must be a function "func(T, T) bool"
    //  > and is implicitly filtered to input values assignable to T
    //
    // https://pkg.go.dev/github.com/google/go-cmp/cmp?tab=doc#Comparer
    //
    opt = cmp.Comparer(func(x, y A) bool {
        s1 := strings.ToLower(x.PublicField)
        s2 := strings.ToLower(y.PublicField)
        return s1 == s2
    })

    output.Stdoutl("Equal(a1, a2, cmp.Compare)", cmp.Equal(a1, a2, opt))

    return nil
}

https://github.com/devlights/try-golang/blob/master/examples/advanced/gocmp/basic.go

実行すると以下のようになります。

$ make run
ENTER EXAMPLE NAME: gocmp_basic
[Name] "gocmp_basic"
Diff(a1, a2)           gocmp.A{
-  PublicField: "hello",
+  PublicField: "HeLLo",
  }

Diff(b1, b2, cmp.AllowUnexported)   gocmp.B{
-  A:            gocmp.A{PublicField: "hello"},
+  A:            gocmp.A{PublicField: "HeLLo"},
-  privateField: "hello",
+  privateField: "world",
  }

Diff(b1, b2, cmpopts.IgnoreUnexported)   gocmp.B{
-  A: gocmp.A{PublicField: "hello"},
+  A: gocmp.A{PublicField: "HeLLo"},
      ... // 1 ignored field
  }

Diff(c1, c2)         差異なし
Equal(a1, a2)        false
Equal(c1, c2)        true
Equal(a1, a2, cmp.Compare) true


[Elapsed] 998.6µs

あと、Ignoreするサンプルも。

package gocmp

import (
    "reflect"
    "time"

    "github.com/devlights/gomy/output"
    "github.com/google/go-cmp/cmp"
    "github.com/google/go-cmp/cmp/cmpopts"
)

type (
    D struct {
        StrField     string    // パブリックな文字列フィールド
        TimeField    time.Time // パブリックな日付フィールド
        privateField string    // プライベートなフィールド
    }
)

// Ignore -- go-cmp にて 指定したフィールド を無視して比較するサンプルです.
func Ignore() error {
    // ---------------------------------------------------------------------------------
    // relect.DeepEqual() は便利であるが、以下の問題がある
    //   - unexported な フィールド まで比較してしまう
    //   - time.Time な フィールド があると一致判定ができない
    //
    // go-cmp では unexported な フィールドを無視したり
    // 明示的に特定のフィールドを無視したり出来る
    // ---------------------------------------------------------------------------------
    var (
        now = time.Now()
        d1  = D{
            StrField:     "hello",
            TimeField:    now,
            privateField: "world",
        }
        d2 = D{
            StrField:     "hello",
            TimeField:    now.Add(2 * time.Second),
            privateField: "golang",
        }
    )

    // ---------------------------------------------------------------------------------
    // 上の d1 と d2 は以下の状態
    //
    // - プライベートなフィールドの値は異なる
    // - StrField は 同じ値
    // - TimeField は 異なる値
    //
    // このようなオブジェクトの場合、日付の値は比較対象から除外して
    // さらにプライベートな値も比較対象から除外して、意味のある公開プロパティが
    // 一致している場合は同値のオブジェクトであると判定することがよくある。
    //
    // ---------------------------------------------------------------------------------

    // ---------------------------------------------------------------------------------
    // reflect.DeepEqual で 比較すると 当然 false となる
    output.Stdoutl("reflect.DeepEqual(d1, d2)", reflect.DeepEqual(d1, d2))

    // ---------------------------------------------------------------------------------
    // go-cmp にて以下の設定を施し比較する
    //
    // - TimeField は 日付情報 なので無視するフィールドとする
    // - プライベートなフィールドは比較しない
    //
    opts := cmp.Options{
        cmpopts.IgnoreFields(d1, "TimeField"),
        cmpopts.IgnoreUnexported(d1),
    }

    output.Stdoutl("cmp.Equal(d1, d2, opts)", cmp.Equal(d1, d2, opts))

    return nil
}

https://github.com/devlights/try-golang/blob/master/examples/advanced/gocmp/ignore.go

実行すると以下のようになります。

$ make run
[Name] "gocmp_ignore"
reflect.DeepEqual(d1, d2) false
cmp.Equal(d1, d2, opts) true


[Elapsed] 0s

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com