json.number 是字符串别名,反射中无法区分,直接转 float64 会静默丢失精度;应通过 .string() 获取后按需解析,或自定义 unmarshaljson 实现高精度处理。

json.Number 在反射中直接转 float64 会丢精度
Go 的 json.Number 本质是字符串,目的是避免浮点解析导致的精度丢失(比如 "12345678901234567890" 转成 float64 就变样)。但很多人在反射里用 v.Interface().(float64) 强转,一碰大整数就出错——不是 panic,而是静默错位。
实操建议:
- 永远别对
json.Number做类型断言到float64或int;它不是数值类型,是带语义的字符串容器 - 用
.String()拿原始字符串,再按需解析:大整数走math/big.Int,小数走strconv.ParseFloat(..., 64),整数走strconv.ParseInt(..., 10, 64) - 如果必须统一转 float64,请先检查字符串是否含小数点或 e/E,再决定用
ParseFloat还是ParseInt+ float64() —— 否则"123"和"123.0"行为不一致
反射遍历 map[string]interface{} 时怎么识别 json.Number
标准 json.Unmarshal 把数字字段默认转成 json.Number,前提是用了 json.Decoder.UseNumber()。但反射里看不出来——v.Kind() 是 string,v.Type() 是 string,不是 json.Number。
原因:json.Number 是类型别名 type Number string,底层就是 string;反射无法区分别名和原类型。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 不要依赖
v.Type().Name()判断是不是json.Number—— 它返回空字符串 - 唯一可靠方式:在 unmarshal 前启用
UseNumber(),然后靠上下文约定:所有从 JSON 来的string类型值,若来自数字字段,就当它是json.Number - 更稳妥的做法:封装一层解包逻辑,在递归反射前,先用
value.(json.Number)尝试断言(仅对已知是 JSON 解析出来的 interface{} 值)
自定义 UnmarshalJSON 实现高精度反序列化
想全程保留精度?别靠反射后期补救。直接让结构体字段自己处理 json.Number。
关键点:实现 UnmarshalJSON([]byte) 方法,内部用 json.Unmarshal 先转成 json.RawMessage,再用 json.Unmarshal + UseNumber() 解出 json.Number。
实操建议:
- 字段类型别用
float64或int64,改用自定义类型如type PreciseNumber struct{ raw json.Number } - 在
UnmarshalJSON里:先json.Unmarshal(data, &n.raw)(因为json.Number实现了该方法),再按需存big.Int或big.Float - 注意:如果字段是嵌套在 slice 或 map 里,且没显式声明类型,仍会 fallback 成
interface{},所以结构体定义要尽量具体
性能与兼容性:UseNumber 不是免费的
启用 UseNumber() 后,所有数字都走字符串路径,内存占用翻倍、GC 压力上升,解析速度下降 10%–20%。多数业务不需要——只有涉及金融、ID、长整型计数器等场景才值得开。
实操建议:
- 别全局开
UseNumber();只在明确需要精度的 decoder 实例上调用 - 如果只是部分字段要高精度,用
json.RawMessage+ 手动解析更轻量,比全量 UseNumber 更可控 - 和前端联调时注意:JavaScript 的
Number本身只有 53 位精度,后端用big.Int解析了,前端可能已经丢了位——精度保障得端到端对齐
最麻烦的不是怎么转,而是什么时候该转、谁负责转。JSON 数字的语义模糊性,从解析那一刻起就埋下了反射里类型信息丢失的伏笔。










