解决 Go 64 位大数 JSON 序列化丢失问题

AI摘要
Go语言中,当uint64或int64数值超过2^53时,JSON序列化会因float64精度限制导致精度丢失。解决方案包括:使用`json:"id,string"`标签转为字符串、自定义类型实现Marshaler接口、或使用第三方库json-iterator将大整数序列化为字符串。

原理:

在 Go 语言中,当把 uint64int64 类型的超大数值(如 uint64(1<<63) 以上,即超过 2^53)序列化 JSON 返回给前端时,会出现精度丢失—— 核心原因是:JSON 标准中没有整数类型,所有数字均以「双精度浮点数(float64)」存储,而 float64 仅能精确表示 [-2^53, 2^53] 范围内的整数,超出该范围的 64 位整数会被截断或四舍五入。

一、问题本质:JSON 数字存储机制

1. float64 的精度限制

  • float64 用 64 位存储数据,其中:1 位符号位 + 11 位指数位 + 52 位尾数位;
  • 尾数位仅能表示 53 个有效二进制位(含整数部分和小数部分),因此超过 2^53 的整数无法被精确存储
  • 临界值:2^53 = 9007199254740992(约 9e15),超过该值的 64 位整数(如 9007199254740993)会被 float64 存储为近似值(如 9007199254740992),导致精度丢失。

2. Go 序列化的默认行为

Go 标准库 encoding/jsonint64/uint64 类型的序列化规则:

  • 当数值 ≤ 2^53 时,序列化为 JSON 数字(float64 可精确表示);

  • 当数值 > 2^53 时,标准库仍会序列化为 JSON 数字,但前端解析时会因 float64 精度限制丢失精度
    示例:

    package main
    
    import (
      "encoding/json"
      "fmt"
    )
    
    func main() {
      type Data struct {
        ID uint64 `json:"id"`
      }
    
      // 超过 2^53 的超大数
      d := Data{ID: 9007199254740993} // 2^53 + 1
      jsonStr, _ := json.Marshal(d)
      fmt.Println("JSON 序列化结果:", string(jsonStr)) // 输出:{"id":9007199254740992}(精度丢失!)
    ## ```
    
    

解决方式1

package model

type Order struct {
  ID       uint64 `json:"id,string"` // 序列化时转为字符串
  OrderNo  string `json:"order_no"`
  TotalAmt float64 `json:"total_amt"`
}

// 测试
func TestStringSerialize() {
  order := Order{
    ID:       9007199254740993, // 超大数
    OrderNo:  "ORDER_20240501",
    TotalAmt: 199.99,
  }

  jsonStr, _ := json.Marshal(order)
  fmt.Println(string(jsonStr)) 
  // 输出:{"id":"9007199254740993","order_no":"ORDER_20240501","total_amt":199.99}(无精度丢失)
}

解决方式2 自定义

package model

import (
  "encoding/json"
  "fmt"
  "math"
)

// 自定义 uint64 类型,实现 Marshaler 接口
type JSONUint64 uint64

// 实现 json.Marshaler 接口
func (u JSONUint64) MarshalJSON() ([]byte, error) {
  // 若数值超过 2^53,序列化为字符串;否则序列化为数字
  if u > JSONUint64(math.MaxInt64) || u > JSONUint64(math.Pow(2, 53)) {
    return json.Marshal(fmt.Sprintf("%d", u))
  }
  return json.Marshal(uint64(u))
}

// 订单模型:使用自定义类型
type Order struct {
  ID       JSONUint64 `json:"id"` // 自定义类型
  OrderNo  string      `json:"order_no"`
  TotalAmt float64     `json:"total_amt"`
}

// 测试
func TestCustomSerialize() {
  // 超大数:序列化为字符串
  order1 := Order{ID: 9007199254740993, OrderNo: "ORDER_1", TotalAmt: 199.99}
  // 小数:序列化为数字
  order2 := Order{ID: 123456, OrderNo: "ORDER_2", TotalAmt: 99.99}

  json1, _ := json.Marshal(order1)
  json2, _ := json.Marshal(order2)
  fmt.Println(string(json1)) // {"id":"9007199254740993","order_no":"ORDER_1","total_amt":199.99}
  fmt.Println(string(json2)) // {"id":123456,"order_no":"ORDER_2","total_amt":99.99}
}

解决方式3 引用第三放库

 go get github.com/json-iterator/go
package main

import (
  "fmt"
  jsoniter "github.com/json-iterator/go"
)

type Order struct {
  ID       uint64 `json:"id"`
  OrderNo  string `json:"order_no"`
  TotalAmt float64 `json:"total_amt"`
}

func main() {
  // 配置:将 uint64/int64 序列化为字符串(避免精度丢失)
  jsonCfg := jsoniter.Config{
    SerializeUint64AsNumber: false, // 关闭 uint64 作为数字序列化
    SerializeInt64AsNumber:  false, // 关闭 int64 作为数字序列化
  }.Froze()

  order := Order{
    ID:       9007199254740993,
    OrderNo:  "ORDER_20240501",
    TotalAmt: 199.99,
  }

  jsonStr, _ := jsonCfg.Marshal(order)
  fmt.Println(string(jsonStr)) 
  // 输出:{"id":"9007199254740993","order_no":"ORDER_20240501","total_amt":199.99}
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!