json.unmarshal会悄悄截断int64字段,因其默认将json数字解析为float64,而float64无法精确表示超16位十进制整数,强制转换时发生补码截断;须改用string字段+strconv.parseint或启用usenumber()手动校验。

Go 模块中数据类型截断不是编译错误,而是运行时数值溢出或结构体字段映射错位导致的静默丢失——必须从序列化、传输、反序列化三端同时校验,单点修复必然漏判。
为什么 json.Unmarshal 会悄悄截断 int64 字段
常见现象是前端传了 1234567890123456789(19 位),Go 结构体字段定义为 int64,但解出来变成 -1234567890123456789 或 0。这不是 Go 的 bug,而是 JSON 标准只规定数字为“近似 IEEE 754 double”,JavaScript 和多数解析器会先转成 float64 再传给 Go,而 float64 无法精确表示超过 253(约 16 位十进制)的整数。
- Go 的
json.Unmarshal对int64字段不做范围校验,直接强制转换 float64 → int64,溢出后按补码截断 - 如果原始 JSON 是字符串形式
"1234567890123456789",则不会截断——但需字段声明为string或自定义UnmarshalJSON - 第三方库如
jsoniter默认行为相同;启用UseNumber()可保留为json.Number类型,但需手动转int64并校验
用自定义 UnmarshalJSON 防止数值截断
对关键 ID、时间戳等不可丢失字段,必须绕过默认 float64 解析路径。最可靠方式是字段类型设为 string,再在 UnmarshalJSON 中用 strconv.ParseInt 精确解析,并检查误差。
type Order struct {
ID string `json:"id"`
}
func (o *Order) UnmarshalJSON(data []byte) error {
type Alias Order // 防止无限递归
aux := &struct {
ID string `json:"id"`
*Alias
}{
Alias: (*Alias)(o),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.ID == "" {
return nil
}
id, err := strconv.ParseInt(aux.ID, 10, 64)
if err != nil {
return fmt.Errorf("invalid id format: %w", err)
}
o.ID = strconv.FormatInt(id, 10) // 或直接存 int64 字段
return nil
}
- 别直接在结构体字段上用
json:",string"tag——它只对数字字面量有效,对字符串值无效 - 若字段必须是
int64类型,ParseInt后要检查是否等于原字符串(避免科学计数法输入如"1e18") - 所有涉及大整数的 API 响应,应在 Swagger/OpenAPI 中明确标记
type: string+format: int64,而非type: integer
数据库读写层如何避免 int64 被 driver 截断
PostgreSQL 的 bigint、MySQL 的 BIGINT 在 Go 驱动中默认映射为 int64,但某些 driver(如老旧 go-sql-driver/mysql)在配置不当或列类型不匹配时,会把超范围值转成 0 或负数,且不报错。
- 确认 driver 版本:MySQL 需
v1.7.0+,PostgreSQL 需pgx/v5或lib/pq最新版 - 查询时显式指定扫描目标类型:
var id int64; row.Scan(&id)比row.MapScan()更可控 - 写入前做范围校验:
if id math.MaxInt64 { return errors.New("id out of int64 range") } - 更彻底方案:数据库该字段改用
text存储,应用层统一走字符串解析——牺牲一点索引效率,换来绝对精度
gob 和 binary 序列化中的截断陷阱
gob 和 encoding/binary 不经过 JSON 浮点中间态,但仍有隐式截断风险:字段类型声明与实际值不符、大小端不匹配、结构体字段顺序变更未同步版本号。
-
gob编码时若字段是int32但存了int64值,会 panic;反之(int64字段存int32值)则静默截断高位 - 用
binary.Write写int64时,必须配binary.LittleEndian或binary.BigEndian,否则跨平台读取必错 - 所有持久化二进制格式必须带版本头:前 4 字节写版本号,解码前先
io.ReadFull校验,不匹配直接拒绝,而非尝试硬解 - 不要依赖
unsafe.Sizeof计算结构体尺寸——字段对齐、padding 因编译器和架构而异,binary.Write必须按字段逐个写
真正难的不是某一处修复,而是让所有环节——HTTP 请求解析、DB 查询、本地文件序列化——对同一字段使用完全一致的类型契约。一旦某环退化为 float64 或 string,后续环节就再也无法还原原始精度。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











