サーバーサイドエンジニアの田実です!
今回はGoの json.Unmarshal
関数で map[string]interface{}
の型を指定したときに発生していた事象とその対策を併せて紹介します!
なにが起きていたか
以下のように任意のキー、バリューを含むJSON文字列を map[string]interface{}
型でパースして、任意のバリューを文字列として取り出す処理がありました。
var res map[string]interface{} json.Unmarshal([]byte(`{"test": 1234567}`), &res) fmt.Printf("%v", res["test"])
任意のバリューがstring, bool、小さいintの値ではうまくいったのですが、上記のように大きいintを含むJSONをパースしたときに、指数表記で文字列が表示されました(本当は 1234567
という文字列がほしい
1.234567e+06
なぜ発生したか
どうやら、json.Unmarshalでinterface{}を指定すると数字はfloat64でパースされ、大きいfloat64を "%v" で表示すると指数表記で表示されるようです。 stackoverflow.com
確かにコードを読むと数値系はfloat64にパースされています。
func (d *decodeState) convertNumber(s string) (interface{}, error) { // ... f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} } return f, nil }
https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/encoding/json/decode.go;l=847-852
対策
json.Unmarshalではなくjson.Decoderとdecoder.UseNumber()を使うと、json.NumberというstringのDefined typeでパースされます。
decoder := json.NewDecoder(bytes.NewReader([]byte(`{"test": 1234567}`))) decoder.UseNumber() err := decoder.Decode(&res);
コードを読むと、convertNumberの関数の先頭の処理に、useNumberが設定されているとjson.Numberで返す処理が入っています。
// convertNumber converts the number literal s to a float64 or a Number // depending on the setting of d.useNumber. func (d *decodeState) convertNumber(s string) (interface{}, error) { if d.useNumber { return Number(s), nil } // ... }
https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/encoding/json/decode.go;l=844-846
このjson.Numberを "%v" で表示すると数値がそのまま文字列で表示されるので、これで対応できました!