Go JSON 数字转 float64 为什么会丢失大整数
来源:17golang原创
时间:2026-09-07 09:17:39 321浏览 收藏
如果接口里的订单号、流水号或雪花 ID 经过 json.Unmarshal 后变成了 float64,再转回整数时出现末尾数字变化,通常不是 JSON 文本自己改了,而是动态解码走了浮点数默认路径。Go 的 encoding/json 在把数字放进 any 时默认使用 float64;当整数超过双精度浮点数能精确表达的范围,精度就可能丢失。
需要保留大整数时,不要先把 JSON 解码到any再断言float64。动态结构使用Decoder.UseNumber(),随后把json.Number按业务语义解析成int64、uint64或其他精确类型;结构固定时则直接定义整数型字段。
先看一个会悄悄改变数字的例子
下面的输入仍是一段合法 JSON,但解码目标是 map[string]any。打印类型可以看到 id 已经不是整数,而是 float64:
package main
import (
"encoding/json"
"fmt"
)
func main() {
raw := []byte(`{"id":9007199254740993}`)
var payload map[string]any
if err := json.Unmarshal(raw, &payload); err != nil {
panic(err)
}
// 动态 JSON 的数字默认落为 float64,这里已经离开整数语义。
fmt.Printf("%T: %v\n", payload["id"], payload["id"])
// 重新编码只能编码当前的浮点值,不能找回被舍入的原文。
encoded, err := json.Marshal(payload)
if err != nil {
panic(err)
}
fmt.Println(string(encoded))
}
关键边界是 253。双精度浮点数并非不能存储更大的数量,而是不能保证每一个相邻整数都有独立表示。小于这个边界的常见计数值往往看不出差异,超过后就可能在转换、比较、签名或重新序列化时暴露问题。

UseNumber 解决的是“先别丢文本”
UseNumber 不会替你决定这个字段是有符号整数、无符号整数还是小数,它做的是把动态 JSON 数字保留为 json.Number。这个类型底层仍保存数字文本,所以可以延后到业务层做一次明确转换:
package main
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
)
func readID(raw []byte) (uint64, error) {
decoder := json.NewDecoder(bytes.NewReader(raw))
// 保留数字原文,避免先经过 float64 的舍入。
decoder.UseNumber()
var payload map[string]any
if err := decoder.Decode(&payload); err != nil {
return 0, err
}
number, ok := payload["id"].(json.Number)
if !ok {
return 0, fmt.Errorf("id 不是 JSON 数字")
}
// 明确这是非负 ID,并让位数溢出成为可处理的错误。
id, err := strconv.ParseUint(number.String(), 10, 64)
if err != nil {
return 0, fmt.Errorf("id 超出 uint64 或格式无效: %w", err)
}
return id, nil
}
这里的边界检查很重要:如果业务允许负数,应改用 number.Int64();如果字段可能含小数,就不能把它强行解析成 ID,而应使用 number.Float64() 并接受浮点语义,或交给定点/高精度数值库处理。

固定结构优先直接声明整数类型
如果 JSON 字段是稳定的,最简单的方案不是维护一个动态 map,而是让结构体表达协议:
type Event struct {
// 发送方保证这是非负的 64 位事件编号。
EventID uint64 `json:"event_id"`
}
func decodeEvent(raw []byte) (Event, error) {
var event Event
// 目标类型明确,解码器不会先把 event_id 放进 float64。
if err := json.Unmarshal(raw, &event); err != nil {
return Event{}, err
}
return event, nil
}
结构体解码还会把超出目标类型范围的值报告为错误。这样比“先接收、后猜类型”更容易发现上游协议不一致,也能让接口测试直接断言 uint64 的结果。
按数据边界选择方案
| 场景 | 推荐方式 | 需要注意 |
|---|---|---|
| 固定 API 响应 | 结构体 + int64/uint64 | 让溢出直接成为解码错误 |
| 字段动态、但必须保留整数 | Decoder.UseNumber | 转换时检查符号和位数 |
| 跨语言传输超大 ID | JSON 字符串 | 协议双方统一按字符串校验 |
| 金额或任意精度小数 | 字符串或定点/高精度类型 | 不要依赖 float64 做精确比较 |
把精度问题挡在接口边界
排查时可以按三点确认:第一,查看解码目标是否为 any 或 map[string]any;第二,打印动态字段的实际类型,而不是只打印格式化后的值;第三,为超过 253 的代表性数字增加“解码—再编码—比较”的测试。只要数字承担身份标识、游标、版本号或签名输入,就应避免未经约束的浮点转换。
一句话判断:固定协议用明确整数类型,动态协议用 UseNumber 保留选择权,跨语言且无法保证整数能力时把 ID 当字符串传输。这样修复的不是某一个特殊数字,而是数据边界上的类型决策。
相关问题
- 为什么重新 Marshal 后数字才看起来变了? 因为舍入通常发生在解码到
float64时,重新编码只是把当前浮点值写回 JSON。 json.Number能保证任意大整数吗? 它先保留文本,但最终转换到int64或uint64仍有范围限制;超过范围要使用字符串或高精度方案。- 把所有 JSON 数字都改成字符串可以吗? 只有协议双方都接受这种表示才可以,不能在单方面改动后期待旧客户端仍按数字处理。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
233 收藏
-
487 收藏
-
409 收藏
-
118 收藏
-
315 收藏
-
116 收藏
-
493 收藏
-
Golang · Go问答 | 2小时前 | ResponseWriter · Go问答 · HTTP响应 · 请求排障 · Go WriteHeader http.ResponseWriter HTTP Header424 收藏
-
Golang · Go问答 | 2小时前 | net/http · Go问答 · HTTP超时 · 服务端配置 · Go http.server WriteTimeout ReadHeaderTimeout IdleTimeout266 收藏
-
499 收藏
-
489 收藏
-
170 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习