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

Go Decoder.UseNumber 为什么仍要检查整数溢出

来源:17golang原创

时间:2026-10-05 19:37:06 297浏览 收藏

Decoder.UseNumber 解决的是“先别把 JSON 数字转成 float64”的问题,不是“这个数字一定能放进任意整数类型”的保证。它把解码到 interface{} 的数字保存成 json.Number,也就是保留原始数字文本;真正转成 int64、uint64 或业务字段时,仍必须检查转换错误和业务范围。

结论先看
  • UseNumber 防止大整数先经过 float64 而丢失精度。
  • json.Number.Int64() 仍可能因小数、指数形式或超出 int64 范围而返回错误。
  • 无符号字段应使用 strconv.ParseUint,任意精度整数可使用 big.Int。
  • 通过语言类型范围后,还要检查数据库列、分页参数、订单数量等业务上限。

官方文档:https://pkg.go.dev/encoding/json

生产目标:保留精度不等于通过范围检查

假设网关把动态 JSON 解码到 map[string]any。默认情况下,其中的数字会变成 float64。像 9007199254740993 这样的整数超过 IEEE 754 双精度浮点数能够连续精确表示的范围,先转成 float64 就可能改变值。调用 UseNumber 后,解码器保留数字字面量,避免这一步精度损失。

但保留下来的只是文本。JSON 数字可以比 int64 更大,也可以是负数、小数或指数形式。官方实现中,json.Number.Int64() 最终按十进制调用 64 位整数解析;超出范围或格式不符合整数要求时会返回错误。因此,UseNumber 与整数溢出检查是前后两道不同的防线。

Decoder.UseNumber 保留 json.Number 原始文本但不自动检查整数与业务范围的结构图
图1:UseNumber 负责保留数字文本,目标类型范围和业务上限仍由调用方显式检查。

环境准备:先确认数字最终落入什么类型

目标语义建议转换必须处理的边界
有符号整数json.Number.Int64()超出 int64、小数、指数形式
无符号整数strconv.ParseUint(n.String(), 10, 64)负数、超出 uint64、小数与指数形式
任意精度整数big.Int.SetString(raw, 10)先拒绝小数与指数形式,再检查业务位数
金额或比例十进制定点类型或自定义解析不要先转 float64;明确小数位和舍入规则

如果 JSON 结构固定,直接解码到带有 int64、uint64 字段的结构体,encoding/json 会在赋值时检查类型范围并返回错误。这通常比动态 map[string]any 更稳妥。只有结构确实动态时,才需要在 json.Number 之后自己完成类型收窄。

安全配置:UseNumber 后显式转换

下面以动态载荷中的 user_id 为例。解码成功只说明输入符合 JSON 语法;要把字段当作 int64 使用,还要验证实际类型并处理 Int64 返回的错误。

package main

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

func requireInt64(v any, field string) (int64, error) {
    n, ok := v.(json.Number)
    if !ok {
        return 0, fmt.Errorf("%s 必须是 JSON 数字", field)
    }

    // Int64 会拒绝小数、指数形式和超出 int64 的值。
    value, err := n.Int64()
    if err != nil {
        return 0, fmt.Errorf("%s 不是可用的 int64: %w", field, err)
    }
    return value, nil
}

func main() {
    input := `{"user_id":9223372036854775808}`
    dec := json.NewDecoder(strings.NewReader(input))
    dec.UseNumber() // 先保留数字文本,避免自动转成 float64。

    var payload map[string]any
    if err := dec.Decode(&payload); err != nil {
        panic(err)
    }
    if _, err := requireInt64(payload["user_id"], "user_id"); err != nil {
        // 生产代码应返回字段级错误,不要继续使用失败时的数值。
        fmt.Println(err)
    }
}

关键点是不能只调用 Int64() 而忽略第二个返回值。转换错误出现后,整数结果不再具有业务意义,必须立即停止该字段的处理。这个规则同样适用于 Float64() 和 strconv 的数值解析函数。

权限边界:有符号、无符号和任意精度要分流

把所有数字都走 Int64() 会误伤合法的 uint64 高位值,也无法支持任意精度 ID。反过来,把负数交给无符号解析也应当失败。生产代码应按字段契约分流,而不是尝试一种转换后再猜测。

package numberguard

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

func parseUint64(n json.Number, max uint64) (uint64, error) {
    // ParseUint 同时拒绝负数、小数、指数形式和 uint64 溢出。
    value, err := strconv.ParseUint(n.String(), 10, 64)
    if err != nil {
        return 0, fmt.Errorf("无效的无符号整数: %w", err)
    }
    if value > max {
        return 0, fmt.Errorf("数值 %d 超出业务上限 %d", value, max)
    }
    return value, nil
}

func parseBigInteger(n json.Number, maxDigits int) (*big.Int, error) {
    raw := n.String()
    if strings.ContainsAny(raw, ".eE") {
        // 本字段只接受十进制整数字面量,不接受 1.0 或 1e3。
        return nil, fmt.Errorf("必须使用整数字面量")
    }
    digits := strings.TrimPrefix(raw, "-")
    if len(digits) == 0 || len(digits) > maxDigits {
        return nil, fmt.Errorf("整数位数超出限制")
    }

    value, ok := new(big.Int).SetString(raw, 10)
    if !ok {
        return nil, fmt.Errorf("无法解析十进制整数")
    }
    return value, nil
}

uint64 的语言范围并不等于业务范围。例如分页大小可能只允许 1 到 1000,数据库列可能实际是 32 位,订单数量还可能受库存约束。先完成格式与机器类型检查,再执行字段自己的最小值、最大值和跨字段规则。

json.Number 分别转换为有符号、无符号和任意精度整数并继续检查业务范围的边界图
图2:不同整数语义使用不同转换入口,转换成功后仍需进入业务范围检查。

日志审计:记录错误类别,不回写完整请求

生产日志应区分三类失败:字段不是 JSON 数字、数字无法落入目标整数类型、数字超过业务上限。建议记录字段名、错误类别、请求追踪 ID 和必要的长度信息;不要把完整请求体或未经限制的超长数字直接写入日志,避免敏感数据泄漏和日志放大。

接口响应也应稳定:语法错误可返回“JSON 格式错误”,类型转换失败返回“字段必须是某类整数”,业务越界返回“字段超出允许范围”。不要把 strconv.NumError 的内部细节直接暴露给外部调用方。

发布检查:边界用例必须覆盖

上线前至少测试以下输入:0、-1、目标上限、上限加一、9223372036854775807、9223372036854775808、18446744073709551615、该值加一、1.0、1e3 和超长数字文本。每个字段都要确认“转换失败后不会继续写数据库或参与权限判断”。

还要区分动态解码和结构体解码:UseNumber 只改变解码到接口值时的数字表示;它不会改变已经声明为 int64 或 uint64 的结构体字段。固定协议优先使用强类型结构体,动态协议才使用 UseNumber 加集中式转换函数。

常见问题

UseNumber 能保证大整数不丢失吗?

它能在接口值中保留原始数字文本,避免先转成 float64 造成精度损失;但能否转成目标整数仍取决于范围和格式,必须检查转换错误。

为什么 1e3 调用 Int64 会失败?

1e3 是合法 JSON 数字,却不是 strconv.ParseInt 接受的十进制整数字符串。若协议允许指数形式,应先定义明确的精确转换规则;若字段就是整数 ID,直接拒绝更清晰。

直接解码到 int64 还需要 UseNumber 吗?

通常不需要。结构体字段已经声明为 int64 时,解码器会按该类型转换并在不匹配或溢出时返回错误。仍要检查整个解码调用的错误,并继续执行业务范围校验。

总结

UseNumber 是精度保留开关,不是整数安全开关。可靠流程应当是:保留数字文本,按字段语义选择有符号、无符号或任意精度解析,检查每次转换错误,再验证业务范围,并把失败分类记录。这样才能真正阻止整数溢出进入数据库、权限判断或资金计算。

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