A money converter needs a numeric quantity as well as a currency. In Go, an Amount holds a Decimal quantity and a Currency. Keeping Decimal as a custom type avoids exposing its storage choice and leaves room to change that choice without changing the public API. section 6.1.1 section 6.2.4
Why not use a float? Floating-point values can introduce imprecision, and a currency may have a smallest meaningful subunit: 86.32456 CAD, for example, is finer than a Canadian cent. Decimal addresses the storage problem, but the implementation shown here does not enforce currency-specific subunits. section 6.2 section 6.2.4
Listing 6.6 decimal.go: Decimal struct implementation
// Decimal can represent a floating-point number with a fixed precision.
// example:
// 1.52 = 152 * 10^(-2) will be stored as {152, 2} #1
type Decimal struct {
// subunits is the amount of subunits.
// Multiply it by the precision to get the real value
subunits int64 #2
// Number of "subunits" in a unit, expressed as a power of 10.
precision byte #3
}
The private fields store an integer count of subunits and a base-10 precision. Thus 1.52 is stored as 152 subunits with precision 2. section 6.2.4
The precision is a byte, allowing different numbers of fractional digits without fixing Decimal to cents. A value such as 0.00000001 BTC needs more digits than a currency commonly divided into hundredths. Keeping the fields private also means callers cannot construct a Decimal by depending on those fields. section 6.2.4
Listing 6.9 decimal.go: ParseDecimal function
// maxDecimal value is a thousand billion, using the short scale -- 10^12.
const maxDecimal = 1e12
// ParseDecimal converts a string into its Decimal representation.
// It assumes there is up to one decimal separator,
// and that the separator is '.' (full stop character).
func ParseDecimal(value string) (Decimal, error) {
intPart, fracPart, _ := strings.Cut(value, ".") #1
subunits, err := strconv.ParseInt(
intPart+fracPart, 10, 64) #2
if err != nil {
return Decimal{}, fmt.Errorf("%w: %s",
ErrInvalidDecimal, err.Error()) #3
}
if subunits > maxDecimal {
return Decimal{}, ErrTooLarge
}
precision := byte(len(fracPart))#4
return Decimal{subunits: subunits, precision: precision}, nil
}
ParseDecimal accepts a string rather than a float. It separates the integer and fractional text at '.', parses their joined digits as an int64, and uses the fractional text’s length as the precision. It returns errors for invalid integers or quantities above 10^12. section 6.2.4
Parsing 1.52
ParseDecimal("1.52") produces Decimal{subunits: 152, precision: 2}. An internal table-driven test checks that case alongside values with no fractional digits, invalid input, and an oversized quantity. The private simplify method can remove trailing fractional zeroes by reducing both subunits and precision. section 6.2.4
The parser assumes at most one '.' separator; its shown code does not separately validate that assumption. Its precision follows the input string, not the currency. Keep those limits in mind when using Decimal to represent money. section 6.2.4 section 6.2
