Goのreflect.DeepEqualでガッツリハマった話

Dec 4, 2018 19:22 · 1803 words · 4 minute read Go

はじめに

この記事は 2018年 アドベントカレンダーとなんの関係もない記事です.

どうも一回も怒られたことがないバイトリーダーのゲインです.

さて本日バイトでModel周りのテストコードを書いていた時に reflect.DeepEqual でガッツリハマったので,共有しておきたいと思います.

ハマったこと

さて皆さんこちらのコードを御覧ください. playgroundはこちら

このコードの実行結果はどうなるでしょう. 実行せずに考えてみてください.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"reflect"
)

type Hoge struct {
	Hoge string `json:"hoge"`
	Huga int    `json:"huga"`
}

func main() {
	m1 := make(map[string]interface{}, 0)
	m1["hoge"] = "hogehoge"
	m1["huga"] = 1

	m2 := make(map[string]interface{}, 0)
	m2["hoge"] = "hogehoge"
	m2["huga"] = 1

	hoge := Hoge{
		Hoge: "hogehoge",
		Huga: 1,
	}
	hogeJSON, err := json.Marshal(hoge)
	if err != nil {
		log.Fatal(err)
	}
	var m3 map[string]interface{}
	err = json.Unmarshal(hogeJSON, &m3)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("m1 == m2?: %t\n", reflect.DeepEqual(m1, m2))
	fmt.Printf("m1 == m3?: %t\n", reflect.DeepEqual(m1, m3))
	fmt.Printf("m2 == m3?: %t\n", reflect.DeepEqual(m2, m3))
}

中身としてはm1,m2は同じキーに対して同じ値を入れた別の変数.

m3に関しては構造体を一度JSONにしてから map[string]interface{} にUnmarshalした変数になってます.

さてこれを実行すると以下のように表示されるでいいですか?

m1==m2?: true
m1==m3?: true
m2==m3?: true

…残念ながら違うんです.

午前中の私は上記になると思ってました.

実際はこうです.

m1 == m2?: true
m1 == m3?: false
m2 == m3?: false

さてどうしてこんなことになるんでしょう.

中身を確認してみましょう

ちょっと改良してこんなコードにしてみました. playgroundはこちら

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"reflect"
)

type Hoge struct {
	Hoge string `json:"hoge"`
	Huga int    `json:"huga"`
}

func main() {
	m1 := make(map[string]interface{}, 0)
	m1["hoge"] = "hogehoge"
	m1["huga"] = 1

	m2 := make(map[string]interface{}, 0)
	m2["hoge"] = "hogehoge"
	m2["huga"] = 1

	hoge := Hoge{
		Hoge: "hogehoge",
		Huga: 1,
	}
	hogeJSON, err := json.Marshal(hoge)
	if err != nil {
		log.Fatal(err)
	}
	var m3 map[string]interface{}
	err = json.Unmarshal(hogeJSON, &m3)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("m1 == m2?: %t\n", reflect.DeepEqual(m1, m2))
	fmt.Printf("m1 == m3?: %t\n", reflect.DeepEqual(m1, m3))
	fmt.Printf("m2 == m3?: %t\n", reflect.DeepEqual(m2, m3))

	fmt.Println("---m1---")
	for _, val := range m1 {
		fmt.Printf("%#v\n", val)
	}

	fmt.Println("---m2---")
	for _, val := range m2 {
		fmt.Printf("%#v\n", val)
	}

	fmt.Println("---m3---")
	for _, val := range m3 {
		fmt.Printf("%#v\n", val)
	}
}

さてこれを実行した結果は以下です. 

m1 == m2?: true
m1 == m3?: false
m2 == m3?: false
---m1---
"hogehoge"
1
---m2---
"hogehoge"
1
---m3---
"hogehoge"
1

おかしい…値も同じなのになんでmapが一致しないんだろう…

reflect.DeepEqual がおかしいんじゃないのか…

とか思い始めてきたので師匠エンジニアにご相談したところ一発で解決したので解説していきます.

なぜこうなったのか

先程のコードにもう少し確認用のコードを追加してみましょう playgroundはこちら

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"reflect"
)

type Hoge struct {
	Hoge string `json:"hoge"`
	Huga int    `json:"huga"`
}

func main() {
	m1 := make(map[string]interface{}, 0)
	m1["hoge"] = "hogehoge"
	m1["huga"] = 1

	m2 := make(map[string]interface{}, 0)
	m2["hoge"] = "hogehoge"
	m2["huga"] = 1

	hoge := Hoge{
		Hoge: "hogehoge",
		Huga: 1,
	}
	hogeJSON, err := json.Marshal(hoge)
	if err != nil {
		log.Fatal(err)
	}
	var m3 map[string]interface{}
	err = json.Unmarshal(hogeJSON, &m3)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("m1 == m2?: %t\n", reflect.DeepEqual(m1, m2))
	fmt.Printf("m1 == m3?: %t\n", reflect.DeepEqual(m1, m3))
	fmt.Printf("m2 == m3?: %t\n", reflect.DeepEqual(m2, m3))

	fmt.Println("---m1---")
	for _, val := range m1 {
		fmt.Printf("val:%#v type:%T\n", val, val)
	}

	fmt.Println("---m2---")
	for _, val := range m2 {
		fmt.Printf("val:%#v type:%T\n", val, val)
	}

	fmt.Println("---m3---")
	for _, val := range m3 {
		fmt.Printf("val:%#v type:%T\n", val, val)
	}
}

最後の確認用のコードに を表示するための %T を追加してみました.

このコードの実行結果はこちらです.

m1 == m2?: true
m1 == m3?: false
m2 == m3?: false
---m1---
val:"hogehoge" type:string
val:1 type:int
---m2---
val:"hogehoge" type:string
val:1 type:int
---m3---
val:"hogehoge" type:string
val:1 type:float64

おや犯人が見えてきましたね.

key huga に入っていた 1 が実はfloat64だったため,reflect.DeepEqualfalseを返していたようです.

さて,ではどうしてjsonに変換した後,それをmapにUnmarshalするとfloat64型になるのかというと, そもそもjsonの仕様が定義されているRFC82591には数字の上限値は定義されていません.

RFC2にあるとおり,の倍精度(つまり64bit)で値を丸めることが精度や運用を考慮し実装されることが多いとのことで,Goもそれに習って実装されてるとということですかね(ここ要出典)

こちらの内容はGo公式のBlog3やjson packageのGodoc4にも書いてあります.

とのことでinterfacejsonUnmarshalされるときはjsonのNumber型はfloat64型で割り当てられることになります.

float64の最大値を超える場合はjson.Number を利用したりbig packageを使って独自のUnmarshalerを作成したりすると良いでしょう.

結論

今回はreflect.DeepEqual(というかjson?)でハマった話でした.

私はこの件で推定5時間程度無駄にしているので,気をつけたい所存です.

この件の問題点を一瞬で見抜いた@shogo82148には頭が上がりません.(謝辞)

まとめは以下

  • パット見の値が同じようでも型が違うことはある
  • jsonのNumberはinterfaceの場合,float64にUnmarshalされる.
  • 公式のドキュメントを読め
  • 最強の師匠がいる環境は最高
  • 公式のドキュメントを読め
  • お前が思っているより公式の関数は1億倍正しく動く
  • 公式のドキュメントを(ry
Twitter共有 Share