目录

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 后超出其精确表示范围。

结合项目场景,推荐以下优先级选择解决方案:

  1. 首选方案:字符串序列化(结构体 tag 加 ,string),实现简单、零依赖、兼容性好,适合绝大多数场景。

  2. 次选方案:自定义类型,适合需要在多个结构体中复用逻辑的场景。

  3. 灵活方案:json.Number 或第三方库 jsoniter,适合不确定数字类型或需要快速改造历史项目的场景。

  4. 兜底方案:前端配合处理,仅在后端无法修改时使用。

实际开发中,建议优先采用字符串序列化方案,尤其是存储用户 ID、订单号等核心业务字段时,既能避免精度丢失,又能减少前后端交互的歧义。

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

版权声明

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

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

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