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

Go encoding/json Decoder UseNumber 保留大整数精度

来源:17golang原创

时间:2026-09-29 04:20:31 328浏览 收藏

当 JSON 被解码到 map[string]any 或 any 时,Go 的 encoding/json 默认把数字保存为 float64。如果字段是订单号、雪花 ID 或数据库主键,数值超过浮点安全整数范围后,精度可能在业务代码做类型断言之前就已经丢失。解决方法是使用 json.Decoder,在 Decode 前调用 UseNumber,让动态数字先保留为 json.Number。

UseNumber 不是把所有数字自动变成大整数,而是保留 JSON 数字的原始文本。后续仍要根据字段约束显式转换为 int64、uint64 或 big.Int,并处理转换错误。

为什么默认解码会改掉大整数

Go 官方文档明确说明:把 JSON 解码进接口值时,布尔值使用 bool,字符串使用 string,对象使用 map[string]any,而数字默认使用 float64。标准库源码中的数字转换逻辑也会在未启用 UseNumber 时调用 64 位浮点解析。

float64 只有有限的整数精度。像 9007199254740993 这样的值无法以 float64 精确表示,因此下面这种动态解码存在风险:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    input := []byte(`{"order_id":9007199254740993}`)

    var payload map[string]any
    // 直接 Unmarshal 到 any 时,JSON 数字默认会进入 float64。
    if err := json.Unmarshal(input, &payload); err != nil {
        panic(err)
    }

    // 此时再把 float64 转成整数,无法恢复已经丢掉的低位精度。
    fmt.Printf("%T %v\n", payload["order_id"], payload["order_id"])
}

问题不在 JSON 语法,而在“未知数字统一落到 float64”这一默认映射。只要目标是具体的整数结构体字段,解码器会按字段类型解析并检查范围;真正需要 UseNumber 的典型场景,是动态网关、通用事件、审计日志或无法预先确定字段模型的对象。

最小可用写法

最小改动是把 json.Unmarshal 换成 json.NewDecoder,并且在第一次 Decode 之前调用 UseNumber:

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

func main() {
    const input = `{"order_id":9007199254740993,"amount":12.50}`

    dec := json.NewDecoder(strings.NewReader(input))
    // 必须在 Decode 前启用,让接口值中的数字保存为 json.Number。
    dec.UseNumber()

    var payload map[string]any
    if err := dec.Decode(&payload); err != nil {
        panic(err)
    }

    orderID, ok := payload["order_id"].(json.Number)
    if !ok {
        panic("order_id 不是 JSON 数字")
    }

    // Int64 会检查语法和范围,不能忽略返回的错误。
    id, err := orderID.Int64()
    if err != nil {
        panic(fmt.Errorf("order_id 不是有效 int64: %w", err))
    }

    fmt.Println(id)
}

json.Number 本质上保存数字字面量。它提供 String、Int64 和 Float64 方法。是否调用哪一个,必须由字段语义决定,而不是看到 json.Number 就统一转成 float64。

JSON 数字字面量通过 Decoder UseNumber 保存为 json.Number 的静态结构说明图
图1:UseNumber 解码结构说明图;动态对象中的数字先保留为 json.Number,再由业务代码选择整数类型。

按字段范围选择 int64、uint64 或 big.Int

生产代码应先确定字段允许的范围,再选择转换方式。下面的表可以直接作为速查:

字段约束建议类型转换方式
有符号 64 位整数int64json.Number.Int64()
非负且允许到 uint64 上限uint64strconv.ParseUint(n.String(), 10, 64)
超出 64 位的纯整数big.IntSetString(n.String(), 10)
带小数或指数的数值按业务精度模型选择不要误用 big.Int

json.Number 没有 Uint64 方法,非负整数可以从它的字符串形式解析。对于超出 64 位的纯整数,则可以交给 math/big:

package numberutil

import (
    "encoding/json"
    "fmt"
    "math/big"
    "strings"
)

func ParseBigInteger(n json.Number) (*big.Int, error) {
    raw := n.String()

    // big.Int 只接受整数;小数点或指数必须由其他精度模型处理。
    if strings.ContainsAny(raw, ".eE") {
        return nil, fmt.Errorf("不是纯整数字面量: %q", raw)
    }

    value, ok := new(big.Int).SetString(raw, 10)
    if !ok {
        // SetString 用 ok 表示解析失败,调用方必须显式处理。
        return nil, fmt.Errorf("无法解析大整数: %q", raw)
    }

    return value, nil
}

如果业务协议允许小数,又要求十进制精度,应使用明确的十进制定点方案、金额最小单位整数或经过评估的十进制库。把金额先转成 float64 再格式化,仍然可能引入舍入问题。

结构体整数、json.Number 字符串和 big.Int 的静态类型边界说明图
图2:数字类型边界说明图;稳定字段优先使用结构体,动态数字保留文本,超范围整数再交给 big.Int。

字段模型稳定时优先定义结构体

UseNumber 主要服务于接口值中的动态数字。如果 API 字段稳定,直接定义结构体通常更简单:解码器会按 int64 或 uint64 解析,并在格式错误或超出范围时返回错误。

package order

import "encoding/json"

type Request struct {
    // 内部 ID 范围明确时,直接使用 uint64 获得范围检查。
    OrderID uint64 `json:"order_id"`

    // 跨语言链路可能无法安全承载大整数时,协议层可明确约定字符串。
    ExternalID string `json:"external_id"`
}

func DecodeRequest(data []byte) (Request, error) {
    var req Request
    // 目标字段是具体类型,不需要 UseNumber 参与动态数字映射。
    if err := json.Unmarshal(data, &req); err != nil {
        return Request{}, err
    }
    return req, nil
}

跨语言系统还要考虑发送方能力。浏览器 JavaScript、其他语言 SDK 或中间消息平台可能先把 JSON 数字读成双精度浮点数,再传给 Go;这时 Go 端使用 UseNumber 也无法恢复上游已经丢失的精度。对于必须跨多种运行时完整传输的超大 ID,把它定义为 JSON 字符串往往更稳妥。

错误处理和日志记录

数字精度问题容易演变成静默数据错误,因此应把转换失败当成输入错误,而不是用零值兜底。建议记录字段名、期望类型和错误类别,但不要原样记录包含隐私或敏感业务信息的整份请求。

  • 类型断言失败:字段可能是字符串、空值或嵌套对象,返回明确的字段类型错误。
  • Int64 失败:可能是小数、指数形式或超出有符号 64 位范围,不要忽略错误继续写库。
  • ParseUint 失败:检查负号、格式和范围,不能直接强制转换。
  • SetString 失败:确认字段确实是纯整数,不要把金额和科学计数法误当成大整数。

如果动态载荷还需要限制未知字段,注意 Decoder.DisallowUnknownFields 主要在目标为结构体时发挥作用;解码到 map[string]any 时,键本来就是动态集合,不能依赖它替代业务字段校验。

发布前检查清单

  1. 确认 UseNumber 在第一次 Decode 前调用。
  2. 确认大整数没有在中间层先进入 float64。
  3. 确认每个动态数字都按字段语义转换,并检查返回错误。
  4. 确认超出 64 位的值只在协议允许时进入 big.Int。
  5. 确认跨语言大 ID 是否需要改为字符串协议。
  6. 确认日志不输出完整敏感载荷,也不以零值掩盖解析失败。

常见问题

json.Unmarshal 能直接启用 UseNumber 吗?

不能。UseNumber 是 json.Decoder 的方法。需要这项行为时,应使用 json.NewDecoder,先调用 UseNumber,再调用 Decode。

调用 UseNumber 后所有字段都会变成 json.Number 吗?

不会。它影响的是解码到接口值中的 JSON 数字。布尔、字符串、数组和对象仍映射到各自类型;具体结构体中的整数、浮点字段也继续按字段类型解码。

json.Number.Int64 能处理 uint64 最大值吗?

不能。Int64 只接受有符号 64 位范围。非负且可能超过 int64 的字段应使用 strconv.ParseUint 解析字符串形式,并检查错误。

UseNumber 能解决上游 JavaScript 已经丢失的精度吗?

不能。它只能保留 Go 解码器收到的 JSON 数字字面量。如果发送方已经把大整数转成不精确的浮点值,原始低位已经不存在。跨语言超大 ID 最好在协议中定义为字符串。

总结起来,UseNumber 的价值是推迟数字类型决策:先保存原始字面量,再由业务边界决定是 int64、uint64、big.Int 还是其他精度模型。稳定字段优先用结构体,动态字段才使用 json.Number,并把每一次转换错误都显式处理。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>