emahiro/b.log

日々の勉強の記録とか育児の記録とか。

go-cmp の cmp.Option と可変長引数 `...` について少し調べた話

Overview

Go Test においてテスト対象の出力結果と期待する値を比較する手法として reflect.DeepEqual ではなく go-cmp を使うことが多くなっていると思いますが、この go-cmpcmp.Option が可変長引数として Diff に指定することができるところで、使い方と可変長引数の指定の仕方でちょっとハマったので、思い出し作業もかねて備忘録としてまとめました。

go-cmp の cmp.Option

比較する時に幾つかの Option を追加することができます。 追加できるオプションは例えば以下のような比較する struct に含まれる非公開フィールドを無視するcmpopts.IgunoreUnexportedや特定のフィールドを比較対象から外す cmpopts.IgnoreField があります*。

*CreatedAt や UpdatedAt のような時刻フィールドは時間が絡むので意図せず壊れやすいため最初から比較対象として外すことが多いです。もちろん時刻が重要なテストでは外してはいけません。

How to use cmp.Option

Diff で可変長引数に指定されている opts に cmp.Option を割り当てることができます。

これの割り当て方はいくつかあります。
例えば cmpopts.IgnoreUnexported を例に取ると https://github.com/google/go-cmp/blob/master/cmp/cmpopts/ignore.go#L119 のように無視したい非公開フィールドを持つ struct を複数登録することが可能です。(引数に可変長引数を取っているため)

cmp.Options の中に option で指定したい設定を入れる

   opts := cmp.Options{
        cmpopts.IgnoreUnexported(A{}),
    }

    if diff := cmp.Diff(x, y, opts); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/YrjDcmT6P9J

これは素直な実装です。Diff の可変長引数に指定されている ...cmp.Option は呼び出し側の Diff メソッドの内部では cmp.Option の配列として扱われるので、type Options []Option と定義されている (ref: https://pkg.go.dev/github.com/google/go-cmp/cmp#Options) と定義されている cmp.Options はそのまま cmp.Option の配列として Diff の引数に指定することができます。

cmp.Option 単体を指定する

   opts := cmpopts.IgnoreUnexported(A{})

    if diff := cmp.Diff(x, y, opts); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/9rKklLLQlQG

可変長引数が呼び出し側で配列(slice) に展開される、ということを考えると若干直感的ではないですが、可変長引数なので単体の cmp.Option を受け取ることも可能です。

slice にして指定する

   opts := []cmp.Option{cmpopts.IgnoreUnexported(A{})}

    if diff := cmp.Diff(x, y, opts...); diff != "" {
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/-qbaKbwWI42

type Options []Option と定義されている && Diff の内部では opts は slice に展開されて利用されるということを考えると、指定時に cmp.Option の slice にしてしまって可変長引数を指定することも可能です。この場合 slice を可変長引数に指定するので $slice... という形で渡す必要があります。

Go の可変長引数の指定方法について

Go の関数の引数に可変長引数を指定した場合、以下のサンプルに示した3つのパターンで可変長引数を指定することが可能です。

サンプルコードだと 2 のパターンは直感的にはわかりやすいですが、3 のパターンは Go に慣れてないととっつきづらいかもしれません。

type str string

func main() {
    m("1")
    m("1", "2")
    m([]str{"1"}...)
}

func m(s ...str) {
    fmt.Println(s)
}

ref: https://play.golang.org/p/7N75PyB2wBW

cmp.Options をそのまま Diff に指定できるわけ

可変長引数を指定する場合 slice 型にした配列でも ... で展開指定しないと可変長引数には実は指定できません。

type strs []str
type str string

func main() {
    m(strs{"1"}...)
}

func m(s ...str) {
    fmt.Println(s)
}

ref: https://play.golang.org/p/pAlGE6ZZcH1

しかし cmp.Options はそのままでも引数に指定することができます。
※ 実は ... を指定することも可能です。

   opts := cmp.Options{
        cmpopts.IgnoreUnexported(A{}),
    }

    if diff := cmp.Diff(x, y, opts...); diff != "" { // ... を指定してる。
        fmt.Printf("mismatch (-x +y):\n%s", diff)
    }

ref: https://play.golang.org/p/Rq4nL01RNXP

cmp.Options... で展開指定せずとも、可変長引数に指定可能なのは、cmp.Optionscmp.Option の slice でありながら、Option として振る舞うことができる ( filter(s *state, t reflect.Type, vx, vy reflect.Value) を実装してる)からです。
Option の slice でありながら、Option 型と同等に振る舞うことができるので、上記の cmp.Option 単体を指定する と同様のことが 型上許容される という挙動をするようです。

まとめ

よく使っていたライブラリも、ちょっと使う期間が空いてしまうとすぐに使い方を忘れますし、実装方法をちょっと深ぼってみると面白いですね。