Go’s Decimal Dilemma: decimal
vs Standard Library Showdown
Go, with its simplicity and performance, has become a popular choice for building a wide range of applications, including financial systems. However, when it comes to handling decimal numbers with precision, Go’s standard library falls short, leaving developers to rely on external packages like shopspring/decimal
. While decimal
offers a convenient solution, it comes with its own set of drawbacks. In this article, we'll explore how to achieve robust decimal calculations using only Go's standard library, and we'll delve into the performance implications and trade-offs of each approach.
The decimal
Package: A Quick Overview
The shopspring/decimal
package provides a Decimal
type for representing and manipulating decimal numbers with arbitrary precision. It offers a wide range of arithmetic operations and formatting capabilities, making it a go-to choice for many Go developers working with financial data.
However, decimal
has a few notable drawbacks:
- Panic on Errors: The package’s error handling relies on
panic
, which can abruptly terminate your application. In mission-critical financial systems, this behavior is undesirable. - External Dependency: Introducing an external dependency increases the complexity of your project and can pose challenges in environments with strict security or compliance requirements.
Embracing the Standard Library: math/big
to the Rescue
Go’s standard library includes the math/big
package, which provides types for working with large integers and rational numbers. The big.Rat
type, in particular, allows us to represent decimal numbers as fractions, ensuring precise calculations without the need for external dependencies.
Performance Showdown: Benchmarking decimal
vs. math/big
To assess the performance implications of each approach, we conducted a series of benchmarks comparing decimal
and math/big
for common financial calculations.
Tax Rate Calculation
We measured the time and memory usage for calculating tax rates using both packages. Here are the results:
BenchmarkTaxRateWithDecimal-12 1187887 989.9 ns/op 1448 B/op 48 allocs/op
BenchmarkTaxRateWithMath-12 871834 1359 ns/op 1528 B/op 62 allocs/op
Surprisingly, the standard library’s math/big
outperformed decimal
in both execution time and memory allocations.
Additional Benchmarks
We encourage you to conduct further benchmarks for other common financial operations, such as currency conversions, interest calculations, and rounding. The results may vary depending on the specific use case, but our initial findings suggest that math/big
can offer a competitive or even superior performance in many scenarios.
Illustrative Examples: decimal
vs. math/big
in Action
Let’s explore a few examples to illustrate how to perform common decimal calculations using both decimal
and math/big
.
Basic Arithmetic
// Using shopspring/decimal
price, _ := decimal.NewFromString("12.34")
quantity := decimal.NewFromInt(5)
total := price.Mul(quantity)
// Using math/big
price, _ := new(big.Rat).SetString("12.34")
quantity := big.NewRat(5, 1)
total := new(big.Rat).Mul(price, quantity)
Benchmark Basic Arithmetic:
BenchmarkBasicArithmeticWithDecimal-12 11303857 103.8 ns/op 128 B/op 5 allocs/op
BenchmarkBasicArithmeticWithMath-12 4044417 281.3 ns/op 256 B/op 14 allocs/op
Percentage Calculations
// Using shopspring/decimal
price, _ := decimal.NewFromString("12.34")
discountRate, _ := decimal.NewFromString("0.15") // 15%
discountedPrice := price.Mul(decimal.NewFromFloat(1).Sub(discountRate))
// Using math/big
price, _ := new(big.Rat).SetString("12.34")
discountRate := big.NewRat(15, 100)
one := big.NewRat(1, 1)
discountedPrice := new(big.Rat).Mul(price, new(big.Rat).Sub(one, discountRate))
Benchmark Percentage Calculations:
BenchmarkPercentageCalcWithDecimal-12 3427479 355.9 ns/op 400 B/op 17 allocs/op
BenchmarkPercentageCalcWithMath-12 2488896 443.6 ns/op 456 B/op 24 allocs/op
Currency Conversion
// Using shopspring/decimal
dollars, _ := decimal.NewFromString("54.95")
exchangeRate, _ := decimal.NewFromString("1.1234") // USD to EUR
euros := dollars.Mul(exchangeRate) // 61.73083
// Using math/big
dollars, _ := new(big.Rat).SetString("54.95")
exchangeRate, _ := new(big.Rat).SetString("1.1234")
euros := new(big.Rat).Mul(dollars, exchangeRate) // 61.73083
Currency Conversion Benchmarks:
BenchmarkCurrencyConversionWithDecimal-12 5802156 183.3 ns/op 176 B/op 8 allocs/op
BenchmarkCurrencyConversionWithMath-12 2641281 440.4 ns/op 384 B/op 17 allocs/op
In each example, we demonstrate how to perform the same calculation using both decimal
and math/big
. The math/big
approach may require a bit more code, but it eliminates the external dependency and potential panic
issues.
Pros and Cons: Weighing Your Options
Decimal Package
Pros:
- User-friendly API
- Wide range of built-in functions
- Extensive documentation and community support
Cons:
- External dependency
- Panic-based error handling
- Potential performance overhead in some cases
math/big Standart Library
Pros:
- No external dependencies
- Panic-free error handling
- Potentially better performance in certain scenarios
Cons:
- Slightly more verbose API
- Less extensive documentation and community support compared to
decimal
Conclusion
While the decimal
package offers a convenient way to handle decimal numbers in Go, the standard library's math/big
package provides a compelling alternative, especially for financial applications where precision, performance, and control over error handling are paramount. By leveraging big.Rat
, you can achieve robust decimal calculations without introducing external dependencies or risking unexpected panics.
We encourage you to experiment with both approaches and consider the specific requirements of your project when making a decision. If you prioritize performance, control, and minimizing external dependencies, math/big
might be the right choice for you.
Remember: The best tool for the job depends on your specific needs and priorities. Don’t hesitate to explore and evaluate different options to find the optimal solution for your Go projects.