目录

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语言的数值表示范围差异。

关键要点总结

  1. 问题根源:JavaScript的Number类型安全范围小于Go的int64范围
  2. 解决方案:字符串序列化、json.Number、自定义类型、第三方库、前端配合
  3. 推荐方案:对新项目,推荐使用字符串序列化方案;对现有项目,根据具体情况选择最适合的改造方案

在实际开发中,建议在项目初期就考虑大数据类型的传输方案,建立统一的规范,避免后期改造的成本。同时,充分的测试也是保证方案可靠性的重要环节。

希望本文能帮助你彻底解决Go语言中的JSON序列化精度问题,让你的开发过程更加顺利便捷!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-json-int64-precision-loss/

备用原文链接: https://blog.fiveyoboy.com/articles/go-json-int64-precision-loss/