Go 踩过的坑之 JSON 序列化 int64 精度丢失
在 Go 语言开发中,JSON 序列化是我们日常工作中最常用的操作之一。
然而,当处理大整数时,很多开发者都遇到过令人困惑的精度丢失问题。
本文将深入分析这一问题的根源,并提供多种实用的解决方案。
一、问题背景
为什么我的大整数变"歪“了?
在实际项目中,你可能遇到过这样的场景:后端返回的完整数据,在前端解析时却变成了近似值。
比如,用户 ID 9223372036854775807到了前端变成了 9223372036854776000。
以下是一个简单的复现示例:
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func main() {
// 模拟一个大的int64数值
user := User{
ID: 9223372036854775807, // 这是一个很大的整数
Name: "张三",
}
// 序列化为JSON
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("序列化结果: %s\n", string(data))
// 现在模拟前端JavaScript环境解析(使用interface{})
var temp map[string]interface{}
if err := json.Unmarshal(data, &temp); err != nil {
log.Fatal(err)
}
// 再次序列化查看结果
data2, _ := json.Marshal(temp)
fmt.Printf("经过interface{}处理后的结果: %s\n", string(data2))
}运行上述代码,你会发现第二次序列化的结果中ID值可能已经发生了变化。
这就是典型的int64精度丢失问题
二、问题分析
问题的根源:JavaScript 的数值范围限制
要理解这个问题,我们需要从两个角度来分析:
2.1 Go 语言的 int64 范围
Go语言中的 int64 类型可以表示的范围是:-2⁶³ 到 2⁶³-1(大约 ±9.2×10¹⁸)
2.2 JavaScript的Number范围
JavaScript使用IEEE 754双精度浮点数表示所有数字,其安全整数范围仅为:-(2⁵³-1) 到 2⁵³-1(即 ±9007199254740991)
关键问题:当 int64 数值超出 JavaScript 的安全整数范围时,就会发生精度丢失。这是因为浮点数表示大整数时无法保证精确性
举个例子,一个大桶装了 1L 水,倒入另外一个桶最大只能装 800 ml,结果能没有问题吗?
三、解决方案(五种)
方法一:字符串序列化(推荐)
这是最常用也最推荐的方案,核心思路是 在序列化时将 int64 类型转换为字符串类型,避免被解析为 float64。
实现起来非常简单,只需要在结构体的 tag 中添加 ,string 标记即可。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
// 添加 ,string 标记,序列化时转为字符串
ID int64 `json:"id,string"`
Name string `json:"name"`
}
func main() {
user := User{
ID: 1234567890123456789,
Name: "张三",
}
data, err := json.Marshal(user)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println("序列化结果:", string(data))
// 反序列化验证
var newUser User
err = json.Unmarshal(data, &newUser)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
fmt.Println("反序列化结果:", newUser.ID)
}运行结果: 序列化结果:{"id":"1234567890123456789","name":"张三"} 反序列化结果:1234567890123456789
优点:实现简单,零依赖,前后端交互清晰,反序列化时能自动转回 int64 类型。 缺点:前端需要根据场景判断是否将字符串转为数字(如果需要计算),但对于 ID 这类无需计算的场景,字符串更合适。
方法二:使用 json.Number 类型
Go 标准库 encoding/json 提供了 json.Number 类型,它本质是一个字符串,用于存储 JSON 中的数字,避免精度丢失。
这种方案适合需要灵活处理数字类型的场景。
package main
import (
"encoding/json"
"fmt"
)
type User struct {
// 用 json.Number 类型存储 ID
ID json.Number `json:"id"`
Name string `json:"name"`
}
func main() {
// 构造数据时需传入字符串形式的数字
user := User{
ID: json.Number("1234567890123456789"),
Name: "张三",
}
data, err := json.Marshal(user)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println("序列化结果:", string(data))
// 反序列化
var newUser User
err = json.Unmarshal(data, &newUser)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
// 转为 int64 类型
id, err := newUser.ID.Int64()
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println("反序列化后转 int64:", id)
}运行结果: 序列化结果:{"id":"1234567890123456789","name":"张三"} 反序列化后转 int64:1234567890123456789
优点:灵活性高,支持将数字转为 int64、float64 等多种类型,适合不确定数字类型的场景。 缺点:构造数据时需要手动转为 json.Number 类型,反序列化后也需要手动转换为目标类型,略显繁琐。
方法三:自定义类型和 MarshalJSON 方法
如果需要在多个结构体中复用 int64 序列化逻辑,可以自定义一个 int64 类型,并实现 json.Marshaler 接口的 MarshalJSON 方法,手动控制序列化过程。
package main
import (
"encoding/json"
"fmt"
"strconv"
)
// 自定义 int64 类型
type Int64 int64
// 实现 MarshalJSON 方法,将 Int64 转为字符串
func (i Int64) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(int64(i), 10)), nil
}
// 实现 UnmarshalJSON 方法,支持反序列化
func (i *Int64) UnmarshalJSON(data []byte) error {
// 去掉引号(如果是字符串形式的数字)
str := string(data)
if str[0] == '"' && str[len(str)-1] == '"' {
str = str[1 : len(str)-1]
}
num, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return err
}
*i = Int64(num)
return nil
}
type User struct {
ID Int64 `json:"id"`
Name string `json:"name"`
}
func main() {
user := User{
ID: Int64(1234567890123456789),
Name: "张三",
}
data, err := json.Marshal(user)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println("序列化结果:", string(data))
var newUser User
err = json.Unmarshal(data, &newUser)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
fmt.Println("反序列化结果:", newUser.ID)
}运行结果: 序列化结果:{"id":"1234567890123456789","name":"张三"} 反序列化结果:1234567890123456789
优点:高度可定制,复用性强,一旦定义好自定义类型,后续使用和原生 int64 差异不大。 缺点:需要手动实现序列化和反序列化方法,初次开发成本稍高。
方法四:使用第三方库(如jsoniter)
如果觉得标准库不够用,也可以使用第三方 JSON 库,比如 jsoniter(国内开发者开发,性能优异,兼容性好)。
它支持通过配置直接解决 int64 精度丢失问题。
首先安装依赖:go get github.com/json-iterator/go
package main
import (
"fmt"
jsoniter "github.com/json-iterator/go"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func main() {
user := User{
ID: 1234567890123456789,
Name: "张三",
}
// 配置 jsoniter 使 int64 序列化为字符串
jsonCfg := jsoniter.Config{
UseNumber: true, // 关键配置,将数字转为 json.Number 类型(字符串形式)
}.Froze()
data, err := jsonCfg.Marshal(user)
if err != nil {
fmt.Println("序列化失败:", err)
return
}
fmt.Println("序列化结果:", string(data))
// 反序列化
var newUser User
err = jsonCfg.Unmarshal(data, &newUser)
if err != nil {
fmt.Println("反序列化失败:", err)
return
}
fmt.Println("反序列化结果:", newUser.ID)
}运行结果: 序列化结果:{"id":"1234567890123456789","name":"张三"} 反序列化结果:1234567890123456789
优点:配置简单,无需修改结构体定义,适合已有项目快速改造。 缺点:依赖第三方库,增加项目体积(但 jsoniter 体积较小,影响不大)。
据这个库的介绍,其性能是优于标准库的,在大量数据序列化的场景下,不妨可以用一用,
方法五:前端配合解决方案
如果后端不方便修改代码(比如历史系统),也可以通过前端配合来解决精度丢失问题。
核心思路是:前端使用 BigInt(现代浏览器支持,因为本质上就是因为:javascript 的 number 不够大)
// 前端代码
const response = await fetch('/api/user');
const data = await response.json();
const userId = BigInt(data.id); // 使用BigInt处理大整数使用专门的 JSON 解析库:
import JSONBig from 'json-bigint';
const data = JSONBig.parse('{"id": 9223372036854775807}');
console.log(data.id.toString()); // 完整保留精度优点:后端无需修改代码。 缺点:依赖前端配合,且仅适用于前端能提前知道需要处理的字段的场景,通用性差。
常见问题
Q1. int64 数值小于 2^53 时,会出现精度丢失吗?
不会。
因为 float64 类型能精确表示 -2^53 到 2^53 之间的所有整数(2^53 约等于 9e15)。
如果你的 int64 数值始终小于这个范围,比如存储的是普通订单号(通常不超过 1e12),则无需担心精度丢失问题。
但为了兼容性(比如未来数值可能扩大),建议还是采用字符串序列化方案。
Q2. 使用字符串序列化后,前端如何进行数字排序?
前端需要先将字符串转为数字再排序。
如果是 ID 这类无需排序的字段,直接用字符串即可;如果是需要排序的数值(比如金额,但金额不建议用 int64 存储,推荐用 decimal 类型),可以在前端用 Number() 或 parseInt() 转为数字后再处理。
Q3. 自定义类型 Int64 能和原生 int64 混用吗?
可以。
但需要手动类型转换。比如将原生 int64 赋值给 Int64 类型时,需要写 Int64(123);将 Int64 转为原生 int64 时,需要写 int64(myInt64),使用时稍显繁琐,但胜在复用性强。
总结
Go 中 JSON 序列化 int64 精度丢失的核心原因是 JSON 标准没有 int64 类型,默认转为 float64 后超出其精确表示范围。
结合项目场景,推荐以下优先级选择解决方案:
-
首选方案:字符串序列化(结构体 tag 加
,string),实现简单、零依赖、兼容性好,适合绝大多数场景。 -
次选方案:自定义类型,适合需要在多个结构体中复用逻辑的场景。
-
灵活方案:json.Number 或第三方库 jsoniter,适合不确定数字类型或需要快速改造历史项目的场景。
-
兜底方案:前端配合处理,仅在后端无法修改时使用。
实际开发中,建议优先采用字符串序列化方案,尤其是存储用户 ID、订单号等核心业务字段时,既能避免精度丢失,又能减少前后端交互的歧义。
希望本文能帮助你彻底解决 Go 语言中的 JSON 序列化精度问题,让你的开发过程更加顺利便捷!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-json-int64-precision-loss/
备用原文链接: https://blog.fiveyoboy.com/articles/go-json-int64-precision-loss/