0

0

Go语言mgo持久化高精度小数:big.Rat的存储方案

心靈之曲

心靈之曲

发布时间:2025-11-18 15:58:01

|

324人浏览过

|

来源于php中文网

原创

Go语言mgo持久化高精度小数:big.Rat的存储方案

本文探讨了在go语言应用中,如何利用`math/big.rat`实现高精度小数计算,并解决其无法直接通过`mgo`库存储到mongodb的问题。核心策略是将`big.rat`对象拆分为分子和分母,以`int64`类型存储在自定义结构体中,从而实现数据的持久化,并在需要时重构为`big.rat`进行精确运算,确保金融敏感数据的准确性。

在Go语言中进行金融计算或任何需要极高精度小数处理的场景时,标准浮点数类型(如float64)由于其固有的精度限制,往往无法满足要求。math/big包中的big.Rat类型提供了一种基于有理数(分数)的精确小数表示,能够避免浮点运算带来的误差。然而,当需要将包含big.Rat类型的数据持久化到MongoDB时,使用mgo这样的ORM库会遇到一些挑战。

1. math/big.Rat与精确小数计算

big.Rat类型通过存储一个分子(Numerator)和一个分母(Denominator)来表示一个有理数。例如,0.5 可以表示为 1/2。这种表示方式确保了在进行加、减、乘、除等运算时,结果始终是精确的,不会有浮点误差的累积。这对于货、账务等对精度要求极高的应用至关重要。

以下是一个使用big.Rat进行计算的简单示例:

package main

import (
    "fmt"
    "math/big"
)

func main() {
    // 创建一个大有理数 5/10
    rat1 := big.NewRat(5, 10)
    fmt.Printf("Initial Rat: %s (Float: %s)\n", rat1.String(), rat1.FloatString(10)) // Output: 1/2 (Float: 0.5000000000)

    // 创建一个小的有理数 1/100000
    rat2 := big.NewRat(1, 100000)
    fmt.Printf("Subtracting Rat: %s (Float: %s)\n", rat2.String(), rat2.FloatString(10)) // Output: 1/100000 (Float: 0.0000100000)

    // 进行减法运算
    rat1.Sub(rat1, rat2)
    fmt.Printf("After Subtraction: %s (Float: %s)\n", rat1.String(), rat1.FloatString(10)) // Output: 49999/100000 (Float: 0.4999900000)
}

2. mgo与big.Rat直接存储的挑战

mgo库在将Go结构体映射到MongoDB文档时,通常依赖于结构体字段的可见性(导出字段)和支持的BSON类型。big.Rat的内部结构包含两个未导出的*big.Int字段来表示分子和分母。由于这些字段是未导出的,mgo无法直接访问它们以进行序列化。即使它们是导出的,big.Int本身也不是MongoDB原生支持的BSON类型,直接存储也会遇到困难。

立即学习go语言免费学习笔记(深入)”;

因此,尝试直接将包含big.Rat字段的结构体存储到MongoDB,通常会导致序列化失败或数据丢失

3. 解决方案:拆分存储分子与分母

解决此问题的有效方法是,不直接存储big.Rat对象,而是将其拆解为分子和分母,并存储这两个基本数值。在需要时,再从存储的分子和分母重构回big.Rat对象。

3.1 定义自定义结构体

为了存储big.Rat的组件,我们可以定义一个简单的自定义结构体,包含两个int64类型的字段来分别存储分子和分母。选择int64是因为它能够覆盖绝大多数实际应用中的数值范围,并且是MongoDB原生支持的BSON整数类型。

// CurrencyValue 用于在MongoDB中存储高精度货币值
type CurrencyValue struct {
    Num   int64 `bson:"num"`   // 分子
    Denom int64 `bson:"denom"` // 分母
}

3.2 转换与存储

在将数据存入MongoDB之前,我们需要将big.Rat对象转换为CurrencyValue结构体。big.Rat提供了Num()和Denom()方法来获取其分子和分母。

Notion AI
Notion AI

Notion是一款集成了笔记、知识库、数据表格、看板、日历等多种能力于一体的应用程序,它既可供个人使用,也可以与他人进行跨平台协作。

下载
// RatToCurrencyValue 将 big.Rat 转换为 CurrencyValue
func RatToCurrencyValue(r *big.Rat) CurrencyValue {
    // 注意:big.Rat 的 Num() 和 Denom() 返回的是 *big.Int
    // 这里我们假设其值在 int64 范围内,并进行转换
    return CurrencyValue{
        Num:   r.Num().Int64(),
        Denom: r.Denom().Int64(),
    }
}

3.3 检索与重构

从MongoDB中检索数据后,我们得到的是CurrencyValue结构体。此时,我们需要将其转换回big.Rat对象,以便进行后续的精确计算。big.NewRat函数可以从两个int64值创建一个新的big.Rat。

// CurrencyValueToRat 将 CurrencyValue 转换为 *big.Rat
func CurrencyValueToRat(cv CurrencyValue) *big.Rat {
    return big.NewRat(cv.Num, cv.Denom)
}

4. 实践示例

下面是一个完整的Go语言示例,演示如何使用mgo将big.Rat类型的数据存储到MongoDB,并进行检索和重构。

package main

import (
    "fmt"
    "log"
    "math/big"
    "time"

    "gopkg.in/mgo.v2"
    "gopkg.in/mgo.v2/bson"
)

// CurrencyValue 用于在MongoDB中存储高精度货币值
type CurrencyValue struct {
    Num   int64 `bson:"num"`   // 分子
    Denom int64 `bson:"denom"` // 分母
}

// Product 代表一个包含精确价格的产品
type Product struct {
    ID    bson.ObjectId `bson:"_id,omitempty"`
    Name  string        `bson:"name"`
    Price CurrencyValue `bson:"price"`
    Qty   int           `bson:"qty"`
}

// RatToCurrencyValue 将 big.Rat 转换为 CurrencyValue
func RatToCurrencyValue(r *big.Rat) CurrencyValue {
    // 检查分母是否为零,避免潜在的运行时错误
    if r.Denom().Cmp(big.NewInt(0)) == 0 {
        log.Fatalf("Error: big.Rat has zero denominator. Cannot convert to CurrencyValue.")
    }
    return CurrencyValue{
        Num:   r.Num().Int64(),
        Denom: r.Denom().Int64(),
    }
}

// CurrencyValueToRat 将 CurrencyValue 转换为 *big.Rat
func CurrencyValueToRat(cv CurrencyValue) *big.Rat {
    // 检查分母是否为零,避免潜在的运行时错误
    if cv.Denom == 0 {
        log.Fatalf("Error: CurrencyValue has zero denominator. Cannot convert to big.Rat.")
    }
    return big.NewRat(cv.Num, cv.Denom)
}

func main() {
    // 1. 连接MongoDB
    session, err := mgo.Dial("mongodb://localhost:27017")
    if err != nil {
        log.Fatalf("Failed to connect to MongoDB: %v", err)
    }
    defer session.Close()

    // 可选:设置会话模式
    session.SetMode(mgo.Monotonic, true)

    db := session.DB("test_db")
    collection := db.C("products")

    // 清理旧数据 (仅为示例方便)
    if _, err := collection.RemoveAll(nil); err != nil {
        log.Printf("Failed to clear collection: %v", err)
    }

    // 2. 创建一个包含 big.Rat 价格的产品
    productPriceRat := big.NewRat(19999, 100) // 199.99
    product := Product{
        ID:    bson.NewObjectId(),
        Name:  "Example Product A",
        Price: RatToCurrencyValue(productPriceRat), // 转换为 CurrencyValue
        Qty:   10,
    }

    // 3. 插入产品到MongoDB
    err = collection.Insert(&product)
    if err != nil {
        log.Fatalf("Failed to insert product: %v", err)
    }
    fmt.Printf("Inserted product: %s with price %s (stored as %v)\n",
        product.Name, productPriceRat.FloatString(2), product.Price)

    // 4. 从MongoDB检索产品
    var retrievedProduct Product
    err = collection.Find(bson.M{"_id": product.ID}).One(&retrievedProduct)
    if err != nil {
        log.Fatalf("Failed to retrieve product: %v", err)
    }

    // 5. 将检索到的 CurrencyValue 价格重构回 big.Rat
    retrievedPriceRat := CurrencyValueToRat(retrievedProduct.Price)

    fmt.Printf("Retrieved product: %s with price %s (reconstructed from %v)\n",
        retrievedProduct.Name, retrievedPriceRat.FloatString(2), retrievedProduct.Price)

    // 6. 进行计算验证
    anotherProductPriceRat := big.NewRat(99, 100) // 0.99
    sumPrice := new(big.Rat).Add(retrievedPriceRat, anotherProductPriceRat)
    fmt.Printf("Calculated total price: %s\n", sumPrice.FloatString(2))

    // 7. 存储一个更新后的产品
    retrievedProduct.Qty = 5
    updatedPriceRat := big.NewRat(150, 3) // 50.00
    retrievedProduct.Price = RatToCurrencyValue(updatedPriceRat)

    err = collection.UpdateId(retrievedProduct.ID, retrievedProduct)
    if err != nil {
        log.Fatalf("Failed to update product: %v", err)
    }
    fmt.Printf("Updated product: %s with new price %s\n",
        retrievedProduct.Name, updatedPriceRat.FloatString(2))

    // 再次检索验证更新
    var updatedRetrievedProduct Product
    err = collection.Find(bson.M{"_id": retrievedProduct.ID}).One(&updatedRetrievedProduct)
    if err != nil {
        log.Fatalf("Failed to retrieve updated product: %v", err)
    }
    updatedRetrievedPriceRat := CurrencyValueToRat(updatedRetrievedProduct.Price)
    fmt.Printf("Verified updated product price: %s\n", updatedRetrievedPriceRat.FloatString(2))
}

5. 注意事项与最佳实践

  • 数据类型选择 (int64 vs big.Int): big.Rat内部使用*big.Int来存储分子和分母,这意味着它们理论上可以任意大。而我们选择int64作为存储类型,虽然能满足绝大多数常见需求(如货币金额),但如果分子或分母的值可能超出int64的最大范围(约9 quintillion),则需要考虑其他存储方案。

    • 替代方案: 如果int64不足以表示,可以将big.Int转换为字符串(big.Int.String())存储到MongoDB,并在检索时通过new(big.Int).SetString(s, 10)将其解析回来。这种方法会增加存储空间和序列化/反序列化开销,但提供了无限精度。
  • 错误处理: 示例代码中为了简洁使用了log.Fatalf,但在生产环境中,应采用更健壮的错误处理机制,例如返回错误而不是直接终止程序。特别是在RatToCurrencyValue和CurrencyValueToRat函数中,应处理分母为零的情况,避免除零错误。

  • 性能考量: 每次存取都需要进行类型转换,这会带来一定的性能开销。对于读写频率极高的场景,需要评估这种开销是否可接受。

  • 更高级的封装 (SetBSON/GetBSON): 为了使Product结构体中的Price字段能够更透明地处理big.Rat,可以为CurrencyValue类型实现mgo的SetBSON和GetBSON接口。这样,在Product结构体中就可以直接使用big.Rat类型,而mgo会在底层自动调用这些接口进行转换,从而隐藏了CurrencyValue这个中间层。

    // 示例:为 big.Rat 实现 mgo 的 BSON 接口(需要一个包装类型)
    type BSONRat big.Rat
    
    func (br *BSONRat) GetBSON() (interface{}, error) {
        if br == nil {
            return nil, nil
        }
        return CurrencyValue{
            Num:   (*big.Rat)(br).Num().Int64(),
            Denom: (*big.Rat)(br).Denom().Int64(),
        }, nil
    }
    
    func (br *BSONRat) SetBSON(raw bson.Raw) error {
        var cv CurrencyValue
        if err := raw.Unmarshal(&cv); err != nil {
            return err
        }
        // 初始化 br
        *br = BSONRat(*big.NewRat(cv.Num, cv.Denom))
        return nil
    }
    
    // 此时 Product 结构体可以这样定义:
    type ProductWithBSONRat struct {
        ID    bson.ObjectId `bson:"_id,omitempty"`
        Name  string        `bson:"name"`
        Price BSONRat       `bson:"price"` // 直接使用 BSONRat
    }

    这种方式使得上层业务代码在使用时更加方便,无需手动进行RatToCurrencyValue和CurrencyValueToRat的转换。

6. 总结

在Go语言中结合mgo和math/big.Rat处理高精度小数时,通过将big.Rat拆分为分子和分母(通常存储为int64类型),并使用自定义结构体进行持久化,可以有效地解决直接存储的难题。这种方法既保证了计算的精确性,又兼容了MongoDB的数据存储机制。根据具体需求,可以进一步利用mgo的BSON接口进行封装,以提供更平滑的开发体验。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

299

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

222

2025.10.31

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

315

2023.08.02

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

254

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

206

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1463

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

617

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

548

2024.03.22

Java 项目构建与依赖管理(Maven / Gradle)
Java 项目构建与依赖管理(Maven / Gradle)

本专题系统讲解 Java 项目构建与依赖管理的完整体系,重点覆盖 Maven 与 Gradle 的核心概念、项目生命周期、依赖冲突解决、多模块项目管理、构建加速与版本发布规范。通过真实项目结构示例,帮助学习者掌握 从零搭建、维护到发布 Java 工程的标准化流程,提升在实际团队开发中的工程能力与协作效率。

10

2026.01.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 3.6万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号