Go JSON 数字转 float64 为什么会丢失大整数
如果接口里的订单号、流水号或雪花 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 数字都改成字符串可以吗? 只有协议双方都接受这种表示才可以,不能在单方面改动后期待旧客户端仍按数字处理。
MySQL invisible index 怎么验证索引删除前的影响
- 上一篇
- MySQL invisible index 怎么验证索引删除前的影响
- 下一篇
- Redis Function 和临时 Lua 脚本有什么部署区别
-
- Golang · Go问答 | 26分钟前 | go · time.Parse · 时间处理 · Go 日期解析 time.Parse time layout
- Go time.Parse 布局写成 yyyy-MM-dd 为什么解析失败
- 233浏览 收藏
-
- Golang · Go问答 | 40分钟前 |
- Go time.Time JSON 输出为什么带有时区和纳秒
- 487浏览 收藏
-
- Golang · Go问答 | 1小时前 | JSON · go · encoding/json · Go DECODE json.Decoder JSON流
- Go JSON Decoder 为什么允许多个 JSON 值连续出现
- 118浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go QueryUnescape 遇到加号为什么变成空格
- 116浏览 收藏
-
- Golang · Go问答 | 1小时前 | HTTP客户端 · Go问答 · URL路径 · 百分号编码 · Go net/url URL RawPath EscapedPath
- Go URL RawPath 为什么会让请求路径出现重复转义
- 493浏览 收藏
-
- Golang · Go问答 | 2小时前 | ResponseWriter · Go问答 · HTTP响应 · 请求排障 · Go WriteHeader http.ResponseWriter HTTP Header
- Go HTTP Header 写入后再修改为什么客户端看不到
- 424浏览 收藏
-
- Golang · Go问答 | 2小时前 | net/http · Go问答 · HTTP超时 · 服务端配置 · Go http.server WriteTimeout ReadHeaderTimeout IdleTimeout
- Go Server ReadHeaderTimeout 和 WriteTimeout 怎么区分
- 266浏览 收藏
-
- Golang · Go问答 | 2小时前 | 网络编程 · HTTP · go · DNS Go 连接超时 ResponseHeaderTimeout http.Client.Timeout
- Go HTTP 客户端超时为什么没有覆盖 DNS 和连接阶段
- 499浏览 收藏
-
- Golang · Go问答 | 2小时前 |
- Go HTTP 请求 Body 不关闭为什么连接池复用变差
- 489浏览 收藏
-
- Golang · Go问答 | 2小时前 | 并发 · 错误处理 · go · Context · Go context.WithCancelCause context.Cause 取消原因
- Go context.WithCancelCause 怎么保留真正的取消原因
- 170浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 171次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 101次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 21次使用
-
- LangGPT
- LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
- 32次使用
-
- ClickPrompt
- ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
- 71次使用
-
- 用Nginx反向代理部署go写的网站。
- 2023-01-17 502浏览
-
- GoLand调式动态执行代码
- 2023-01-13 502浏览
-
- Go 取 range 变量地址为什么得到重复指针
- 2026-09-07 501浏览
-
- Go net.Conn 写入超时为何仍会卡住:SetWriteDeadline、部分写入与连接复用检查
- 2026-08-30 501浏览
-
- Go 问答:httptrace.ClientTrace GotConnInfo 怎么判断连接是否复用:连接池与请求时序边界
- 2026-08-28 501浏览

