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,结果能没有问题吗?
三、解决方案(五种)
(一)方法一:字符串序列化(推荐)
这是最常用且兼容性最好的解决方案。通过在结构体标签中添加,string选项,将 int64 字段序列化为字符串
package main
import (
"encoding/json"
"fmt"
)
type UserString struct {
ID int64 `json:"id,string"` // 关键:添加,string标签
Name string `json:"name"`
}
func stringSolution() {
user := UserString{
ID: 9223372036854775807,
Name: "李四",
}
data, _ := json.Marshal(user)
fmt.Printf("字符串序列化: %s\n", string(data))
// 输出: {"id":"9223372036854775807","name":"李四"}
// 反序列化同样有效
var newUser UserString
json.Unmarshal(data, &newUser)
fmt.Printf("反序列化后ID: %d\n", newUserID)
}优点:
- 前后端兼容性好
- 符合JSON规范
- 实现简单
(二)方法二:使用json.Number类型
当使用 interface{} 接收不确定类型的JSON数据时,可以使用 json.Number 来避免精度丢失
package main
import (
"encoding/json"
"fmt"
"strings"
)
func useNumberSolution() {
jsonStr := `{"id": 9223372036854775807, "name": "王五"}`
// 传统方式(会出问题)
var data1 map[string]interface{}
json.Unmarshal([]byte(jsonStr), &data1)
fmt.Printf("传统方式: %v\n", data1["id"])
// 使用UseNumber的方式
var data2 map[string]interface{}
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.UseNumber() // 启用UseNumber
decoder.Decode(&data2)
if id, ok := data2["id"].(json.Number); ok {
// 可以按需转换为int64或float64
intID, _ := id.Int64()
fmt.Printf("UseNumber方式: %d\n", intID)
}
}json.Number的本质是字符串,它在反序列化时保留原始数值字符串,需要时再转换
(三)方法三:自定义类型和MarshalJSON方法
对于需要更精细控制的场景,可以自定义类型并实现 json.Marshaler 接口
package main
import (
"encoding/json"
"strconv"
)
// 自定义int64类型,序列化为字符串
type Int64String int64
func (i Int64String) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(int64(i), 10)), nil
}
func (i *Int64String) UnmarshalJSON(data []byte) error {
// 去除字符串的引号
s := string(data)
if len(s) > 0 && s[0] == '"' {
s = s[1 : len(s)-1]
}
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
*i = Int64String(val)
return nil
}
type UserCustom struct {
ID Int64String `json:"id"` // 使用自定义类型
Name string `json:"name"`
}
func customTypeSolution() {
user := UserCustom{
ID: Int64String(9223372036854775807),
Name: "赵六",
}
data, _ := json.Marshal(user)
fmt.Printf("自定义类型序列化: %s\n", string(data))
}高度自定义
(四)方法四:使用第三方库(如jsoniter)
标准库在某些场景下有限制,可以考虑使用第三方库如jsoniter
package main
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/json-iterator/go/extra"
)
func jsoniterSolution() {
// 启用模糊解码器
extra.RegisterFuzzyDecoders()
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Data struct {
ID int64 `json:"id"`
}
// 即使JSON中数字是字符串形式,也能正确解析
jsonStr := `{"id": "9223372036854775807"}`
var data Data
json.UnmarshalFromString(jsonStr, &data)
fmt.Printf("jsoniter解析结果: %d\n", data.ID)
}据这个库的介绍,其性能是优于标准库的,在大量数据序列化的场景下,不妨可以用一用,
(五)前端配合解决方案
有时也需要前端的配合来解决这个问题(因为本质上就是因为:javascript 的 number 不够大)
前端使用BigInt(现代浏览器支持)
// 前端代码
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()); // 完整保留精度总结
Go语言中int64的JSON序列化精度问题是一个典型的跨语言数据交互陷阱。通过本文的分析,我们可以看到这个问题的根源在于JavaScript和Go语言的数值表示范围差异。
关键要点总结:
- 问题根源:JavaScript的Number类型安全范围小于Go的int64范围
- 解决方案:字符串序列化、json.Number、自定义类型、第三方库、前端配合
- 推荐方案:对新项目,推荐使用字符串序列化方案;对现有项目,根据具体情况选择最适合的改造方案
在实际开发中,建议在项目初期就考虑大数据类型的传输方案,建立统一的规范,避免后期改造的成本。同时,充分的测试也是保证方案可靠性的重要环节。
希望本文能帮助你彻底解决Go语言中的JSON序列化精度问题,让你的开发过程更加顺利便捷!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-json-int64-precision-loss/
备用原文链接: https://blog.fiveyoboy.com/articles/go-json-int64-precision-loss/