decimal is an immutable arbitrary-precision decimal type built on top of math/big.Int.
It keeps both unscaled integer digits and decimal precision, making it suitable for financial
and accounting workloads that require deterministic base-10 behavior.
Import path:
import "github.com/exc-works/decimal"- English User Guide: docs/user-guide.en.md
- Chinese (Simplified) User Guide: docs/user-guide.zh.md
- Chinese (Traditional) User Guide: docs/user-guide.zh-Hant.md
- Japanese User Guide: docs/user-guide.ja.md
- Korean User Guide: docs/user-guide.ko.md
- Spanish User Guide: docs/user-guide.es.md
- French User Guide: docs/user-guide.fr.md
- German User Guide: docs/user-guide.de.md
- Portuguese (Brazil) User Guide: docs/user-guide.pt-BR.md
- Russian User Guide: docs/user-guide.ru.md
- Arabic User Guide: docs/user-guide.ar.md
- Hindi User Guide: docs/user-guide.hi.md
package main
import (
"fmt"
"github.com/exc-works/decimal"
)
func main() {
price := decimal.MustFromString("12.5000")
fee := decimal.NewWithPrec(75, 2) // 0.75
total := price.Add(fee)
rounded := total.Rescale(2, decimal.RoundHalfEven)
fmt.Println(price.String()) // 12.5
fmt.Println(price.StringWithTrailingZeros()) // 12.5000
fmt.Println(total.String()) // 13.25
fmt.Println(rounded.String()) // 13.25
}Common constants:
decimal.Zerodecimal.Onedecimal.Tendecimal.Hundred
Decimal uses immutable value semantics:
- methods like
Add,Sub,Mul,Quo, andRescalereturn new values - pointer receiver methods (
Unmarshal*,Scan) update the receiver BigInt()returns a copy, so internal state is not exposed for mutation
Example:
a := decimal.MustFromString("1.20")
b := a.Add(decimal.MustFromString("0.30"))
fmt.Println(a.String()) // 1.2 (a is unchanged)
fmt.Println(b.String()) // 1.5decimal.New(int64)decimal.NewFromInt(int)decimal.NewWithPrec(int64, prec)decimal.NewFromFloat64(float64)decimal.NewFromFloat32(float32)decimal.NewWithAppendPrec(int64, prec)decimal.NewFromUintWithAppendPrec(uint64, prec)decimal.NewFromBigInt(*big.Int)decimal.NewFromBigRat(*big.Rat)decimal.NewFromBigRatWithPrec(*big.Rat, prec, decimal.RoundingMode)decimal.NewFromBigIntWithPrec(*big.Int, prec)decimal.NewFromInt64(int64, precision)decimal.NewFromUint64(uint64, precision)decimal.NewFromString(string)decimal.MustFromString(string)decimal.NewFromDecimal(Decimal)(deep copy)d.Clone()(deep copy; useful afterNewFromBigIntwith an externally mutable*big.Int)
NewFromString supports:
- plain decimals:
123,-123.45 - scientific notation:
1.234e3,123456E-3
It trims leading/trailing spaces and rejects malformed formats such as:
- empty string
1..1- multiple decimal points
- missing or invalid exponent
If parsing results in zero, precision is normalized to 0.
Add(Decimal)/SafeAdd(Decimal)/AddRaw(int64)Sub(Decimal)/SafeSub(Decimal)/SubRaw(int64)Mul(Decimal, decimal.RoundingMode)MulDown(Decimal)MulExact(Decimal)(exact multiplication, no rounding, precision =d.prec + d2.prec)Mul2(Decimal)(deprecated alias ofMulExact)QuoWithPrec(Decimal, prec, decimal.RoundingMode)Quo(Decimal, decimal.RoundingMode)QuoDown(Decimal)QuoRem(Decimal)Mod(Decimal)Power(int64)Sqrt() (Decimal, error)/SqrtWithPrec(prec)ApproxRoot(int64) (Decimal, error)/ApproxRootWithPrec(root, prec)Log2() DecimalLog10() (Decimal, error)/Log10WithPrec(prec)(input must be > 0)Ln() (Decimal, error)/LnWithPrec(prec)(input must be > 0)Exp() (Decimal, error)/ExpWithPrec(prec)(Taylor series with argument reduction)
RescaleDown(prec)Rescale(prec, decimal.RoundingMode)Shift(places)TruncateWithPrec(prec)/RoundWithPrec(prec)FloorWithPrec(prec)/CeilWithPrec(prec)Truncate()/Round()/Floor()/Ceil()StripTrailingZeros()SignificantFigures(figures, decimal.RoundingMode)
Cmp(Decimal)Equal(Decimal)/NotEqual(Decimal)GT(Decimal)/GTE(Decimal)LT(Decimal)/LTE(Decimal)Max(Decimal)/Min(Decimal)- package-level helpers:
decimal.Max,decimal.Min,decimal.Between
IntPart()Remainder()Sign()/IsNegative()/IsZero()/IsNotZero()/IsPositive()IsInteger()/HasFraction()Neg()/Abs()BigInt()/BigRat()Float32() (float32, bool)/Float64() (float64, bool)Int64() (int64, bool)/Uint64() (uint64, bool)BitLen()Precision()MustNonNegative()
decimal.RoundDown(toward zero)decimal.RoundUp(away from zero)decimal.RoundCeiling(toward +infinity)decimal.RoundHalfUpdecimal.RoundHalfDowndecimal.RoundHalfEven(banker's rounding)decimal.RoundUnnecessary(panics if rounding is required)
String()strips trailing zerosStringWithTrailingZeros()keeps trailing zerosFormatWithSeparators(thousands, decimal rune)for locale-aware display (e.g.,12345.67→"12,345.67"or European"12.345,67")Format(fmt.State, verb rune)implementsfmt.Formatter, supporting%v,%s,%q,%d,%f,%e,%g,%bwith width/precision/flags
MarshalJSON()encodes as a JSON stringUnmarshalJSON()accepts JSON string and (in some paths) raw JSON number text- uninitialized value marshals as
null
MarshalXML()/UnmarshalXML()MarshalXMLAttr()/UnmarshalXMLAttr()for use in XML attributes- Uninitialized values encode as empty element/attribute
MarshalBSONValue()/UnmarshalBSONValue()viago.mongodb.org/mongo-driver/v2/bson- Encodes as BSON string; uninitialized encodes as BSON null
- Decodes from String, Double, Int32, Int64, Decimal128, Null
NullDecimalalso implements BSON value marshaling
MarshalYAML()returns string formUnmarshalYAML()parses scalar string/number values
MarshalText()UnmarshalText()UnmarshalParam(string)(for ginBindUnmarshaler)
ShouldBindQuery/ShouldBind/ShouldBindUriuseUnmarshalParam(string)ShouldBindJSONusesUnmarshalJSON()
Example:
type Req struct {
Amount decimal.Decimal `form:"amount" uri:"amount" json:"amount"`
}
var req Req
if err := c.ShouldBindQuery(&req); err != nil {
// handle error
}- Use
decimal_requiredto require Decimal field presence - Built-in
omitemptycan be used as usual - Decimal numeric comparison tags:
decimal_eq,decimal_ne,decimal_gt,decimal_gte,decimal_lt,decimal_lte,decimal_between(tilde-separated bounds, e.g.decimal_between=1~100;minmust be<=max) - Sign/zero tags (no param):
decimal_positive,decimal_negative,decimal_nonzero - Precision tag:
decimal_max_precision=N— max number of decimal places (scale), i.e. digits after the decimal point; not total significant digits.123.45has scale2and passesdecimal_max_precision=2. - Uses exact
Decimalcomparison (Cmp), withoutFloat64conversion - Supports friendly error messages via translation helpers:
RegisterGoPlaygroundValidatorTranslations,RegisterGoPlaygroundValidatorTranslationsWithMessages, andTranslateGoPlaygroundValidationErrors - Built-in translation locales (13):
en,zh,zh_Hant,ja,ko,fr,es,de,pt,pt_BR,ru,ar,hi - Register once before any validation; calling
RegisterGoPlaygroundValidatormultiple times on the same*validator.Validateis idempotent — later calls simply overwrite the previously registered handlers.
Safety note. Validator tag parameters must be compile-time constants. Passing malformed parameters (non-numeric limits, unparseable decimal values,
min > maxfordecimal_between, negativedecimal_max_precision) causes panics at validation time — do not splice untrusted input into struct tags.
Example:
import (
"github.com/exc-works/decimal"
"github.com/go-playground/validator/v10"
)
type Req struct {
Amount decimal.Decimal `validate:"decimal_required,decimal_eq=12.34"`
}
v := validator.New()
_ = decimal.RegisterGoPlaygroundValidator(v)
err := v.Struct(Req{Amount: decimal.MustFromString("12.34")})Friendly messages example:
import (
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
)
enLocale := en.New()
uni := ut.New(enLocale, enLocale)
trans, _ := uni.GetTranslator("en")
_ = decimal.RegisterGoPlaygroundValidatorTranslations(v, trans)
messages := decimal.TranslateGoPlaygroundValidationErrors(err, trans)Custom language template override example:
_ = decimal.RegisterGoPlaygroundValidatorTranslationsWithMessages(v, trans, map[string]string{
"decimal_required": "{0} cannot be empty",
})For gin:
import (
"github.com/exc-works/decimal"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = decimal.RegisterGoPlaygroundValidator(v)
}MarshalBinary()/UnmarshalBinary()Marshal()/Unmarshal()MarshalTo([]byte)Size()
Binary format:
- first 4 bytes: big-endian
uint32precision - remaining bytes: gob-encoded
big.Int - trailing zeros are stripped before serialization
decimal.PrecisionFixedSize == 4
Value()implementsdriver.ValuerScan(any)implementssql.Scanner
Scan supports: nil, float32, float64, int64, string, []byte, and quoted/unquoted decimal text.
For nullable SQL columns, use NullDecimal:
type Row struct {
Amount decimal.NullDecimal
}
var r Row
_ = db.QueryRow("SELECT amount FROM t").Scan(&r.Amount)
if r.Amount.Valid {
fmt.Println(r.Amount.Decimal.String())
}NullDecimal implements sql.Scanner, driver.Valuer, JSON/YAML/Text/BSON
marshaling, and gin UnmarshalParam. null/empty input sets Valid=false.
The package exposes sentinel errors so callers can switch on error category
via errors.Is:
ErrInvalidFormat— malformed decimal string inNewFromString,UnmarshalJSON, etc.ErrInvalidPrecision— negative precisionErrOverflow— int64/uint64/float conversion overflowErrDivideByZero— division by zeroErrNegativeRoot— even root of a negative value (Sqrt,ApproxRoot)ErrInvalidRoot— non-positiverootpassed toApproxRootErrInvalidLog— logarithm of a non-positive valueErrRoundUnnecessary— rounding required underRoundUnnecessarymodeErrUnmarshal— binary/YAML/BSON/SQL unmarshal failuresErrInvalidArgument— invalid setup argument (e.g. nil validator/translator)
_, err := decimal.NewFromString("not a number")
if errors.Is(err, decimal.ErrInvalidFormat) {
// handle
}Decimalvalues are safe for concurrent read access by multiple goroutines as long as no goroutine reassigns the variable.- Value-receiver methods (
Add,Sub,Mul,Cmp,String, etc.) never mutate the receiver and are safe to call concurrently. - Pointer-receiver methods (
Scan,UnmarshalJSON,UnmarshalYAML,UnmarshalText,UnmarshalBinary) mutate the receiver; external synchronization is required when the same*Decimalmay be accessed concurrently. - Accessors like
BigInt()/BigRat()return defensive copies. - Package-level constants (
Zero,One,Ten,Hundred) are read-only.
- negative precision panics in constructors/rescaling
NewFromStringreturns error;MustFromStringpanicsMustNonNegativepanics for negative valuesLog2()panics unless value> 0ApproxRoot(root)requiresroot > 0- even root of negative values returns error
Quohas a special integer-division path when both precisions are0- binary encoding normalizes trailing zeros (
7.50and7.5000can encode identically)
If you are migrating from older internal variants of this library:
- rely only on APIs present in this repository
- update formatting-sensitive code if it depended on fixed-scale output (
StringWithTrailingZeros) - validate binary compatibility if old code expected trailing-zero preservation
- Semantic Versioning: https://semver.org/
- Changelog format: https://keepachangelog.com/
- changelog file:
CHANGELOG.md - GitHub release trigger: tags matching
v*.*.*
Example:
git tag -a v0.1.0 -m "release v0.1.0"
git push origin v0.1.0BSON support is compiled out by default so that downstream projects are not
forced to pull in go.mongodb.org/mongo-driver/v2. To enable it, build with
the bson build tag:
go build -tags bson ./...
go test -tags bson ./...When the tag is set, Decimal and NullDecimal implement
bson.ValueMarshaler / bson.ValueUnmarshaler (see marshal_bson.go).
Without the tag, no BSON code is compiled and the MongoDB driver is not
linked into the resulting binary, keeping the core library dependency-free.