What is the proper golang equivalent to Decimal when dealing with money?

Theres the various floats, including the new one in big. But “conventional” wisdom tells you to always avoid floats when trying to represent money.

What is the proper go solution to this?

You can of course use the Rational struct (Rat) to represent decimal values in the form of fractions, which you can use to parse things like scientific notation into a Rational representation.

But what is the “right” way to handle money in go?

3 Likes

What’s your use-case? Many payment gateway API libs (e.g. Stripe’s) use int64 and describe the value in cents (7900 = $79.00) to keep it simple - which is a common, well-understood approach.

6 Likes

Translating into other units, once in an accurate data type, isn’t really my concern - it’s holding it in that accurate data type.

The general use case is alot of addition and subtraction of small units of currency - sub-penny. Or to be more specific, whatever the unit is (although hopefully it’s the smallest unit), it can be represented in terms of values less than one, but greater than zero.

I guess I was just wondering if you’re already using things like the different Decimal datatypes in other systems (ruby, scala, mysql, etc), what’s the most accurate way to map or represent those values in your go apps?

I’m not sure what OP’s use cases are, but I have some where I need both sub-penny precision and multiple currencies.

A common solution I’ve seen outside of Go is to store the amount as an int, a string currency code, and a byte for the number of decimal places to the right of the decimal point. Not really sure this is any better than a big.Rat though, so I too am curious if there’s a broad consensus on the topic.

I’ve used https://github.com/shopspring/decimal a few times. In their documentation they have a rationale of why it is better to use a decimal type for money vs big.Rat.

2 Likes

I’ll take a look at shopspring, thanks!

As for my usecase, it’s actually for dealing with data out of a legacy system that stores money in that format. We had to do that there, for certain reasons, and I wrote an entire library to handle abstracting the complexity of dealing with data and performing math between 2 values of that format.

I’d prefer to not have to do it again in go :slight_smile:

FYI, there are at least two proposals to provide decimal numeric type as part of Go:

Based on the feedback from the Go team a high-quality implementation could be considered for inclusion into the core library.

1 Like

Thanks for the links, Konstantin. A proper builtin solution is what I was hoping for, so I’ll keep an eye on those proposals.

I looked at GitHub - leekchan/accounting: money and currency formatting for golang earlier. At the end of the README he says

Please do not use float64 to count money. Floats can have errors when you perform operations on them. Using big.Rat (< Go 1.5) or big.Float (>= Go 1.5) is highly recommended. (accounting supports float64, but it is just for convenience.)

But I think Decimal is a better choice, for the reasons stated in shopspring/decimal

1 Like

Yea, big.Rat was basically my current workaround, but I wasn’t sure if big.Float was viable. I imagine it has much better precision but… the word “float” still scares me lol.

big.Float is a binary floating point number so while it can have higher precision than float64 it is still not the best choice for use cases requiring decimal arithmetic. big.Float with Prec set to 64 behaves exactly the same as float64 except that it cannot hold NaN values.

I had a hunch that the rationale was going to go in the direction of 1/3. Their argument doesn’t make sense as you cannot have US$1/3. You can concoct a situation where something costs US$1 and you offer it for 1/3 of the price, but even then, their argument still doesn’t apply, because what you will charge the customer is US$0.33, not US$1/3. You could argue about what happens if the customer sees the offer and buys 3. Should they be charged US$1 or 99 cents? Here things get hairy because consumer protection laws kick in and the mathematically correct answer is not necessarily the legal answer (item costs US$1, you offer it for 1/3 of that price, customer buys 3000, should they pay 990 or 1000? Math says the later, law tends to say the former, but not always)

If your input is decimal (0.33), I would go with big.Rat, representing that not as 1/3 but as 33/100. If you need to do something like tax calculations, and your tax rate is say 9.75%, I’d represent that as 975/10000 (39/400) and do math with that, postponing rounding to pennies as late as possible. Be warned, though, your rationals will probably get big fast (e.g. 33/100 * 39/400 = 1287/40000, 33/100 + 1287/40000 = 14487/40000 ~ 0.36).

Marcelo, that is eerily similar to how I handled this in another language for a legacy system which lacked the ability to use the proper Decimal format, although the base values were not numerator/denominator or even a decimal, but a raw value and an exponent. From those two, you can derive the Rational and then the Decimal quite easily.

And big.Rat makes this simple in Go as well, but at the end of the day a lot of things “think” in terms of a decimal-like number, and you’ll need to get data out of, or put data into, a system which only thinks in those terms (the merit of only supporting those types of values is a conversation for another day).

When pennies aren’t the smallest subdivision of currency you support, you have to get a little more clever.

All in all, what you’re suggesting basically lines up with my experiences, but I was hoping for a “true” Decimal type, as it’s simply a lot more natural a way to express operations between these types of values - at least, imho.

But this thread has given me a lot of good feedback, so I think I have a good sense of how to handle this in Go for the time being, until they add in a proper Decimal type.

Thanks everyone!

Back when European VAT was introduced, one of the problems programmers faced was that many of them had done that. Unfortunately, European VAT required 4 digits of precision after the decimal point.

I can see rationals as being problematic too — you really don’t want to end up with $⅓.

The thing that makes float64 problematic isn’t the fact that it’s floating point, it’s that it’s binary floating point with fixed precision. Arbitrary precision decimal floating point would be OK.

(If I were designing a programming language, I’d make the float types decimal, and have a binaryfloat or approxfloat type for the approximate high speed binary floats.)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.