json.Unmarshal精度丢失问题分析
tbghg

问题描述

根据id查询某条数据查询不到,观察日志发现id后两位丢失变为00

分析

先看一段代码:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
)

func main() {
    var m map[string]interface{}
    str := `{"id":9223372036854775807, "app":12312.3434}`
    _ = json.Unmarshal([]byte(str), &m)

    fmt.Printf("Unmarshal type:%T, value:%v\n", m["id"], m["id"])
    fmt.Printf("Unmarshal type:%T, value:%v\n", m["app"], m["app"])

    fmt.Println("-----------------------------------")

    d := json.NewDecoder(bytes.NewReader([]byte(str)))
    d.UseNumber()
    _ = d.Decode(&m)

    fmt.Printf("UseNumber type:%T, value:%v\n", m["id"], m["id"])
    fmt.Printf("UseNumber type:%T, value:%v\n", m["app"], m["app"])
}

/*
Unmarshal type:float64, value:9.223372036854776e+18
Unmarshal type:float64, value:12312.3434
-----------------------------------
UseNumber type:json.Number, value:9223372036854775807
UseNumber type:json.Number, value:12312.3434
*/

使用json.Unmarshal解析后,若未制定目标的类型,对于数字默认会采用float64,见json.Unmarshal中的文档:

// To unmarshal JSON into an interface value,  
// Unmarshal stores one of these in the interface value:  
// bool, for JSON booleans  
// float64, for JSON numbers  
// string, for JSON strings  
// []interface{}, for JSON arrays  
// map[string]interface{}, for JSON objects  
// nil for JSON null

结论

因此,对于使用int64类型的数据,若数字超过了float64的范围,则会造成精度损失。

解决方案:使用UseNumber,见示例代码中的第二部分。关于UseNumber文档给出的描述如下:

// UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
func (dec *Decoder) UseNumber() { dec.d.useNumber = true }

使用UseNumber后会使用json.Number类型接收值:

// A Number represents a JSON number literal.
type Number string

即通过字符串进行接收,也可称为数字文本

多级嵌套时,若内部含有interface{}且为int类型同样需要注意使用UseNumber

不过在前端序列化大数时也会出现精度损失的情况,如下:

image

所以对于大数的情况前后端最好提前沟通一下这种情况,也可以对于大数统一使用字符串类型序列化后传递。或者双方约定,数字类型不得超过53位

注意事项

若代码下文包含switch-case类型转换,要单独把json.Number类型列出,例如:

// FormatInt64 将v转换成int64
func FormatInt64(v interface{}) (res int64, err error) {
    switch vt := v.(type) {
    case int64:
        res = vt
        return
    case int:
        res = int64(vt)
        return
    case int32:
        res = int64(vt)
        return
    case int16:
        res = int64(vt)
        return
    case int8:
        res = int64(vt)
        return
    case float64:
        res, err = strconv.ParseInt(strconv.FormatFloat(vt, 'f', 0, 64), 10, 64)
        return
    case float32:
        res, err = strconv.ParseInt(strconv.FormatFloat(float64(vt), 'f', 0, 64), 10, 64)
        return
    case []byte:
        res, err = strconv.ParseInt(string(vt), 10, 64)
        return
    case string:
        res, err = strconv.ParseInt(vt, 10, 64)
        return
    default:
        err = fmt.Errorf("not support %T", v)
    }
    return
}

因为不包含json.Number会直接报错not support json.Number

 评论