Golang中json.Marshal与Unmarshal处理大数字时数值不一致问题
问题复现
复现代码如下:
package main import ( "encoding/json" "fmt" ) func t() { a := int64(1<<63 - 1) fmt.Println(a) m := map[string]interface{}{ "balance": a, } data, _ := json.Marshal(m) m1 := make(map[string]interface{}) err := json.Unmarshal(data, &m1) panicIF(err) fmt.Println(int64(m1["balance"].(float64))) } func panicIF(err error) { if err != nil { panic(err) } }
代码运行控制台输出:
9223372036854775807 -9223372036854775808
问题底层原理
你提到的「JSON不建议使用number类型存储大于2^53的数字,这类大数值应当改用string类型存储」的说法完全准确,这是跨语言JSON传输的通用约定,底层逻辑如下:
- JSON标准本身没有对number类型做整数、浮点数的明确区分,绝大多数编程语言的标准JSON解析库,在不知道字段预期类型的场景下(比如解析到
interface{}/any类型、无结构的map类型),都会默认将JSON number按照IEEE 754 双精度浮点数(float64)规则存储。 - float64的有效尾数长度为52位,加上隐含的1位前置整数位,总共可以无误差精确表示的整数范围是
[-2^53, 2^53],超出这个范围的整数无法被float64精确存储,必然出现精度丢失。 - 你的代码中写入的
1<<63 - 1是int64类型的最大正整数9223372036854775807,远大于2^53(对应值为9007199254740992)。反序列化时因为目标类型是map[string]interface{},json包无法感知balance字段原本是int64类型,会默认将其解析为float64,转换过程中该值被近似为9223372036854775808,这个值已经超出int64正数的表示上限,强制转换为int64时发生整数溢出,最终得到int64的最小值-9223372036854775808。
正确规避方式
- 跨语言通用方案:所有超过2^53的整数,在JSON序列化时统一转为string类型传输,接收端拿到字符串后再转换为对应语言的64位整数类型,从根源上避免float64精度丢失问题。
- Go生态内无跨语言需求的场景:反序列化时使用
json.Decoder并开启UseNumber()选项,此时JSON number会被直接解析为json.Number类型(本质是原始数字字符串),不会提前转成float64,使用时调用其Int64()方法即可拿到正确的int64值,示例代码:
import "bytes" // 替换原有的json.Unmarshal逻辑 m1 := make(map[string]interface{}) decoder := json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() err := decoder.Decode(&m1) panicIF(err) // 取值时转换 balance, err := m1["balance"].(json.Number).Int64() panicIF(err) fmt.Println(balance) // 正确输出9223372036854775807
- 固定结构场景尽量避免用
map[string]interface{}接收JSON数据,提前定义对应结构体,将大数字段明确声明为int64/uint64类型,json包反序列化时会直接按照指定类型解析,不会走float64的默认转换逻辑。
内容的提问来源于stack exchange,提问作者Laxmi Prasad
相关产品推荐
相关产品推荐

