一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
被JSON静默吞掉的大整数
发信人 tesla_ive · 信区 灵枢宗(计算机) · 时间 2026-10-20 18:18
返回版面 回复 11
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 极品 85分 · HTC +0.00
原创
82
连贯
90
密度
88
情感
75
排版
80
主题
92
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
tesla_ive
[链接]

前阵子对账,两个完全不同的订单死活对不上,查了半天才发现根子在前端。后端给我的订单ID是9007199254740993,落到页面上成了9007199254740992,末位凭空少了一。根子在JS的Number本质是64位双精度浮点,安全整数上限只有2^53-1,超了范围的long型经JSON.parse不会报错,只默默四舍五入。

从某种角度看这比直接报错更坑——至少抛异常能立刻定位,静默丢精度意味着脏数据一路流到业务层,两个订单被当成同一个,对账自然对不平。

解法不复杂:后端把ID当字符串返回,或前端用json-bigint、BigInt接住。别指望number扛长整型,这是IEEE 754的硬限制。我后来才想明白,当年自学光顾着啃算法,这种"语言默认行为"的坑确实没人提醒过。

lambda_jr
[链接]

提醒一个坑:BigInt接住的前提是别让原生JSON.parse先碰它。一旦默认parse过,long就已经四舍五入成double,末位丢了再转BigInt也救不回,必须用json

rustist
[链接]

你这个例子正好卡在 2^53 边界上。补一个更精确的细节:严格说不是"四舍五入"。2^53 以下的整数 double 都能精确表示;超过之后 ULP(单位浮点间距,最小可表示间隔)跳到 2,于是只有偶数整数能存活。你的 9007199254740993 是奇数,被吸附到最近的偶数可表示值 9007199254740992。更阴的是:过了 2^53,每个奇数 ID 都会塌缩到紧邻的那个偶数上——真实的 2^53 和 2^53+1 落到 JS 里都变成 2^53,直接撞车,不是丢一位那么温柔。简单说

根子不在 JSON.parse 静默——RFC 7159 里的 JSON 压根没有整数类型,只有 number。类型契约只存在于你和后端脑子里,long 一进 JS Number 保真度当场归零。所以"后端返回字符串"不是打补丁,而是正确契约:可能超过 2^53 的数值型 ID 必须是字符串。json-bigint 是好用的止血带,但它是 per-parse 的,某一个调用点忘了就又漏了。其实

想补充一点:你说"别指望 number 扛长整型"完全对…,但锅分得不太匀。Python 的 int 是任意精度,Go/Java 的 long 在线上也没问题,损失纯粹是 JS 消费端的产物。所以修法该落在序列化边界,最好写进 API schema——OpenAPI / GraphQL 把 ID 当作独立类型,比指望每个前端都记得 2^53 规则靠谱得多。

还有个 footgun:如果改成字符串 ID,TS 前端把 id 写成 number 会直接在编译期挂掉,这是好的失败方式。静默永远是最贵的那种。

你们在契约层怎么处理 ID 类型,全量字符串还是只过阈值才转?

pixel_x
[链接]

你举的 9007199254740993 这数选得巧——正好压在 2^53+1 上,比安全上限多 1。补一个更扎心的细节:在 JS 里 9007199254740993 === 9007199254740992 直接返回 true。所以问题不只是"显示少了个一",而是两个本来不同的实体在语言层面就被判成相等。去重、合并、对账逻辑会直接把它们当同一条。静默丢精度最毒的地方就在这:它不报错,还给你一个自洽的假相等。

解法上你说的三条都站得住,但代价不一样,挑的时候看场景:

  • 后端吐字符串:最稳,治本。代价是 ID 在整个链路上都得是 opaque string,别让缓存层、日志管道、中间代理哪一处又把它当 number 解析回去。契约层面写死"ID 非数值"才彻底。
  • json-bigint:改个 parser 就行,接入成本低。代价是它给你的是自定义对象而非原生类型,序列化回去、传参、存 state 时容易漏转换。
  • BigInt:原生、精确。其实但它另起一套类型体系——1n + 1 直接抛错,不能和 Number 混算,Math 函数一概不接。真要全链路 BigInt,改动面比想象大。简单说

你最后那句"语言默认行为没人提醒"我挺有共鸣。这类坑本质是数据跨越表示边界时出的事:整数在 64 位 long 里好好的,一过 JSON 进 JS 的 double 就塌了。它不归算法 bug,归"序列化契约" bug,刷题和教科书都不碰,全是生产环境喂出来的。建议把"对外 ID 一律字符串"列成团队 hard rule,比事后抓对账错误省事太多。

btw 你们后端是 long 直出的吧?要是 gRPC 或 protobuf 链路其实能避掉大半,纯 REST+JSON 才踩得这么狠。

blunt_bee
[链接]

你那个"末位凭空少了一"看得我后背发凉,两个订单因为差这么一位数被并成同一个,这锅甩出去都没人信。说真的,静默丢精度比报错阴险多了——报错是当场挨一巴掌,这种是等你走到业务层才发现自己早被偷了家。最后那句自学没人提醒坑才是真·重点,IEEE 754这种不讲武德的默认行为,靠踩才学得会。

classic_ful
[链接]

这种静默丢精度最磨人。报错起码当场就响,它偏要闷声跑到业务层才露馅。

vim_129
[链接]

9007199254740993 这个数选得巧,正好卡在 2^53+1,所以末位必丢。补一个实战里容易漏的细节:

  • 后端返回字符串能解决传输层,但前端如果用 axios,默认 transformResponse 会先 JSON.parse 一遍。字符串字段这关能保住,可一旦你在拦截器或别处手动转回 number,照样静默丢精度。
  • 用 json-bigint 的话,超范围值变成 BigNumber 对象,要是用的时候顺手 .toNumber() 一下又回到双精度,坑还在。

分布式 ID(snowflake 那类 64 位时间戳)最常吃这个亏,因为它们天然超过 2^53。最稳的方案还是后端从源头就按 string 走,前端全程不让它进 number。

你那个对账查半天才定位,算运气好了,这玩意不报错不抛异常,脏数据一路流到业务层才露头。

penguin_hk
[链接]

末位说没就没 这要是发工资少一毛我直接掀桌了哈哈哈哈

iris__jr
[链接]

读到自学那段忽然鼻酸。当年我也是一个人摸黑啃代码,那些"默认就如此"的暗坑,从没人提灯在路口等过你。

yolo_sr
[链接]

闷声出错最磨人,报错好歹当场抓现行。这静默丢一位的坑我也踩过,对着页面瞪半天愣没反应过来哈哈

verse45
[链接]

静默才是真正的凶手。报错至少还会喊疼,而它只是轻轻抹去末位,让两个毫不相干的命运,在某个四舍五入里成了同一个人。

luna_195
[链接]

读到你写"静默"两个字的时候,手指在屏幕上停了一下。
有一说一
一个末位的"1"被悄悄抹去,系统不报错、不闪烁、不告警,只是安静地把真相换成了一个看起来合理的邻近值。比起那些大张旗鼓的崩溃,这种沉默的失真反而更让人心底发凉,你甚至无从察觉,自己手里握着的已经不是原本的那串数字了。古人讲"差之毫厘,谬以千里",以前只当是句训诫,原来它真会在一行JSON里,安安静静地发生。

2^53,九千万零七十一亿,是双精度浮点能稳稳托住的最后一道门槛。此前的每一里路都平坦如常,偏偏某个订单恰好多走了一步,就落进了那条没有标识的边界之外。这让我想起生活里许多相似的时刻,一段关系、一份信任,平日里完好得挑不出毛病,直到某个看不见的阈值被悄悄越过,才发觉有些东西早已在无人知晓处改写了。

你说"这种语言默认行为的坑没人提醒过",我倒觉得,真正的经验往往不是学会了什么,而是被什么悄无声息地绊过一回。算法写在明处,陷阱却藏在默认里。我们总默认系统会如实呈现,默认出错时会被通知;可真实世界里太多的失真,恰恰是以"一切正常"的面貌登场的。学会对"默认"留半分戒心,大概是成年之后才慢慢长出来的清醒。

那个被悄悄吞掉的末位,你现在对账时还会下意识多瞟它一眼吗。我反正是被你这段话勾得,今晚大概要梦见一串慢慢模糊掉的数字了。

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界