👻

go-cmpの出力にNBSPが含まれる問題に対処する

2023/06/11に公開

go-cmpの紹介

Goの構造体を比較するためのライブラリとして go-cmp がある。これは reflect.DeepEqual とは異なり、一部の要素は無視するなどより柔軟な比較が可能となる。また、差分をわかりやすく表示してくれるという特徴もある。

go-cmpのサンプル
package main

import (
	"fmt"

	"github.com/google/go-cmp/cmp"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	p1 := Person{
		Name: "Alice",
		Age:  20,
	}
	p2 := Person{
		Name: "Bob",
		Age:  20,
	}

	diff := cmp.Diff(p1, p2)
	fmt.Println(diff)
}
$ go run sample.go
  main.Person{
- 	Name: "Alice",
+ 	Name: "Bob",
  	Age:  20,
  }

go-cmpの出力が安定しない

go-cmpのdiff出力が便利なのでこれを活用してみたところ上手くいかなかった。

go-cmpをアプリケーション本体(テスト以外の文脈)にて利用した。また、意図した通りdiffが出力されることを確認するためのテストを書いた。サンプルコードは以下の通り(例ではgo-diff自体をテストしているが実際は自前の関数がgo-cmpを呼び出した結果をテストしていた)。
実装当初は問題なかったが、何度かテストを実行しているとたまにエラーが発生するという問題が生じた。

go-cmpの出力をテストする
package main

import (
	"fmt"

	"github.com/google/go-cmp/cmp"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	p1 := Person{
		Name: "Alice",
		Age:  20,
	}
	p2 := Person{
		Name: "Bob",
		Age:  20,
	}

	expected := `  main.Person{
- 	Name: "Alice",
+ 	Name: "Bob",
  	Age:  20,
  }
`

	actual := cmp.Diff(p1, p2)

	if actual != expected {
		fmt.Println("actual:\n" + actual)
		fmt.Println("expected:\n" + expected)

	} else {
		fmt.Println("OK")
	}
}
$ go run sample2.go
OK

$ go run dsample2.go
actual:
  main.Person{
- 	Name: "Alice",
+ 	Name: "Bob",
  	Age:  20,
  }

expected:
  main.Person{
- 	Name: "Alice",
+ 	Name: "Bob",
  	Age:  20,
  }

見た目上の違いは確認できなかったが、[]byte 変換して比較したら実際にはスペースではなくNBSP(non-breaking space)になっていることがわかった。実行するたびに結果が変化し、スペースで出力されたりNBSPで出力されたりするという挙動だった。

対策

NBSPを置き換える

なぜランダムでNBSPになるのかは理解できていなかったが、NBSPで出力されることがわかったので、その場合にスペースで置き換えればよいと判断した。Unicodeとして処理するので rune として扱う必要がある。

出力のNBSPをスペースで置換する
...
func main() {
...
	actual = replaceNBSPWithSpace(cmp.Diff(p1, p2))
...
}

func replaceNBSPWithSpace(s string) string {
	return strings.Map(func(r rune) rune {
		if r == '\u00A0' {
			return ' '
		}
		return r
	}, s)
}

この結果、テストが成功したり失敗したりすることは解消することができた。

go-cmpの仕様

go-cmpのリポジトリで確認したところ、この問題は既知であることがわかった。
go-cmpはあくまでテスト用途などを想定している。このため、diff出力結果に依存して欲しくないためランダム性を持たせている。

https://github.com/google/go-cmp/issues/235

実際、go-cmpのドキュメントにはこれらの注意事項が記載されている。

It is intended to only be used in tests, as performance is not a goal and it may panic if it cannot compare the values.

Do not depend on this output being stable. If you need the ability to programmatically interpret the difference, consider using a custom Reporter.

まとめ

go-cmpで期待した通りの差分が出力されることを確認するテストがフレーキーになった。これはgo-cmpの意図した挙動であり、出力結果に依存してはいけないというgo-cmpの方針によるものだった。差分結果を確認したかったので推奨される対策ではないことを承知の上でNBSPを置き換えるという実装で解決した。

diffの出力が主目的であればgo-diffに移行した方がよさそう(自分も最終的には移行した)。
とはいえ、暫定対処としては単にNBSPを置き換える方法も有効だった。

Discussion