go 的 json.unmarshal 默认将 json 数字转为 float64 导致大整数精度丢失,应使用 json.number 接收并调用 .int64() 转换,超 int64 时用 big.int.setstring(),前后端须约定大数传字符串。

Go 的 json.Unmarshal 默认把所有 JSON 数字塞进 float64,只要原始值超过 9007199254740991(即 2^53 - 1),精度就不可逆丢失——哪怕你字段声明为 int64 或 uint64,也救不回来。
结构体字段用 json.Number 接收大整数
这是最直接、副作用最小的方案。它不解析数字,只存原始字符串序列,彻底绕过 float64 中转。
- 字段类型必须显式声明为
json.Number,例如:ID json.Number `json:"id"` - 后续要转整数时,**必须调用
.Int64()**,它内部做了溢出检查和格式校验;别自己用strconv.ParseInt(n.String(), 10, 64),容易漏错 - 如果 ID 可能超
int64范围(比如 128 位雪花 ID),就直接用n.String()拿原始文本,传给前端或写入数据库都安全 -
json.Number不支持算术运算,id + 1这种写法编译不过;要算就得先转,且必须检查返回的error
处理 map[string]interface{} 时启用 UseNumber()
当你无法提前定义结构体(比如接第三方 Webhook、泛型数据、动态字段),就得在解码器层面干预。
- 必须用
json.NewDecoder,然后调用.UseNumber(),例如:dec := json.NewDecoder(r); dec.UseNumber() - 这会让所有 JSON 数字在
map[string]interface{}中变成json.Number类型,而不是float64 - 但注意:标准库不会自动递归转换嵌套的
map或[]interface{}里的数字,你得手动遍历、识别、调用.Int64()或.String() - 别在全局随便开
UseNumber()后又忽略转换——比如直接id := m["id"].(int64),会 panic,因为实际类型是json.Number
前端必须发字符串,否则 Go 层再怎么处理都白搭
JS 的 Number 一样受 2^53 - 1 限制。如果前端仍用 { "id": 12345678901234567890 } 这种写法,JSON 解析那一刻就已失真,Go 根本没机会介入。
- 前后端约定:所有可能超限的字段(ID、微秒级时间戳、大金额)统一用字符串传输,例如:
{"id": "12345678901234567890"} - Go 端配合加
",string"标签:ID string `json:"id,string"`,这样即使前端漏发引号,也能避免float64解析 - 如果无法改前端,就只能靠
json.RawMessage延迟解析,但要注意:后续若用json.Unmarshal再解一次,仍可能掉进float64陷阱
超 int64 范围的大数必须用 big.Int
json.Number.Int64() 在值超出 int64 时会返回 error,这时不能硬转,得换路子。
- 别用
strconv.ParseInt(..., 10, 64)或strconv.Atoi,它们只支持 ≤64 位,超限直接 panic - 用
big.Int.SetString(s, 10),但必须检查第二个返回值ok,否则静默失败(变量保持零值) - 字符串需提前
strings.TrimSpace(),否则含空格或换行会失败 -
big.Int没有不可变语义,a.Add(a, b)是修改a本身;想保留原值,得写c := new(big.Int).Add(a, b) - 输出纯十进制字符串必须用
bigInt.Text(10),别用fmt.Println(bigInt),它可能走科学计数法
最容易被忽略的是:精度丢失发生在 JSON 解析的第一步,不是类型转换那步;而修复的前提是前后端协同——单边努力注定失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











