在Protobuf定义文件中传输任意精度小数,应使用哪种正确scalar type?
这确实是金融系统里很常见的序列化选型困惑,我来帮你拆解下每个选项的优劣势,以及适配Go+shopspring/decimal的最佳实践:
1. 绝对不推荐:double类型
虽然double对应Go的float64,但金融场景下绝对要避开——哪怕只是序列化环节也不行。原因很简单:浮点数的二进制存储本身就存在精度丢失问题,比如0.1这样的十进制小数无法被二进制浮点数精确表示。哪怕你在业务代码里用shopspring/decimal保证了计算精度,一旦序列化成double再反序列化,就可能引入不可逆的精度误差,这对金融数据来说是致命的。
2. 稳妥但繁琐:string类型
用string存储decimal值是完全安全的,因为shopspring/decimal本身就支持从字符串解析(decimal.NewFromString())和转成字符串输出(decimal.String()),精度100%有保障。
但缺点也很明显:
- 序列化后的数据体积会比数值类型大一些
- 其他语言的客户端需要额外做字符串转decimal的处理,增加了跨语言协作的成本
- Protobuf的校验规则(比如范围校验)没法直接作用在字符串上,需要自己实现业务层校验
3. 更优解:自定义Protobuf Decimal类型
类似mgravell/protobuf-net里的decimal类型,你可以在Protobuf里定义一个自定义的decimal消息类型,来映射shopspring/decimal的内部结构——毕竟shopspring/decimal本质上是由整数系数和缩放因子组成的(比如123.45会被存成系数12345,缩放因子2)。
举个例子,定义Protobuf类型:
message Decimal { // 对应shopspring/decimal的Coefficient(整数部分,可能为负) int64 coefficient = 1; // 对应缩放因子,即小数点后的位数 int32 scale = 2; }
然后在Go代码里写一个转换工具函数:
import ( "math/big" "github.com/shopspring/decimal" pb "your/proto/package/path" ) // 将shopspring/decimal转成自定义Protobuf Decimal func DecimalToProto(d decimal.Decimal) *pb.Decimal { return &pb.Decimal{ Coefficient: d.Coefficient().Int64(), Scale: int32(d.Scale()), } } // 将Protobuf Decimal转成shopspring/decimal func ProtoToDecimal(p *pb.Decimal) decimal.Decimal { return decimal.NewFromBigInt(big.NewInt(p.Coefficient), p.Scale) }
这种方式的优势:
- 完全保留精度,和
shopspring/decimal的内部逻辑对齐 - 序列化后的数据体积小,效率高
- 跨语言客户端可以很容易地映射到各自语言的decimal类型(比如Java的
java.math.BigDecimal、C#的decimal) - 可以在Protobuf里给
coefficient和scale加校验规则(比如scale不能为负)
总结建议
如果你的服务是纯Go生态,且对开发效率要求极高,string类型可以凑合用;但如果是跨语言的gRPC服务,或者追求最佳的精度和性能,自定义Protobuf Decimal类型是最优选择。千万不要为了省事用double,金融场景下的精度误差是不可接受的。
内容的提问来源于stack exchange,提问作者Matt Mc

