登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 按业务语义解析成 int64uint64 或其他精确类型;结构固定时则直接定义整数型字段。

先看一个会悄悄改变数字的例子

下面的输入仍是一段合法 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。双精度浮点数并非不能存储更大的数量,而是不能保证每一个相邻整数都有独立表示。小于这个边界的常见计数值往往看不出差异,超过后就可能在转换、比较、签名或重新序列化时暴露问题。

JSON 数字进入 float64 后在 2 的 53 次方附近出现精度边界的原创技术框图
图1:动态 JSON 数字从 any 进入 float64 后的精度边界

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() 并接受浮点语义,或交给定点/高精度数值库处理。

UseNumber 保留 json.Number 后按 int64 或 uint64 业务语义分流的原创技术框图
图2:UseNumber 保留数字文本,再按字段语义选择精确转换

固定结构优先直接声明整数类型

如果 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转换时检查符号和位数
跨语言传输超大 IDJSON 字符串协议双方统一按字符串校验
金额或任意精度小数字符串或定点/高精度类型不要依赖 float64 做精确比较

把精度问题挡在接口边界

排查时可以按三点确认:第一,查看解码目标是否为 anymap[string]any;第二,打印动态字段的实际类型,而不是只打印格式化后的值;第三,为超过 253 的代表性数字增加“解码—再编码—比较”的测试。只要数字承担身份标识、游标、版本号或签名输入,就应避免未经约束的浮点转换。

一句话判断:固定协议用明确整数类型,动态协议用 UseNumber 保留选择权,跨语言且无法保证整数能力时把 ID 当字符串传输。这样修复的不是某一个特殊数字,而是数据边界上的类型决策。

相关问题

  • 为什么重新 Marshal 后数字才看起来变了? 因为舍入通常发生在解码到 float64 时,重新编码只是把当前浮点值写回 JSON。
  • json.Number 能保证任意大整数吗? 它先保留文本,但最终转换到 int64uint64 仍有范围限制;超过范围要使用字符串或高精度方案。
  • 把所有 JSON 数字都改成字符串可以吗? 只有协议双方都接受这种表示才可以,不能在单方面改动后期待旧客户端仍按数字处理。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>