Complete Guide to the math and math/big Packages in Go

Use the math package for standard calculations and math/big for arbitrary-precision arithmetic with very large numbers.

When standard types hit the wall

You write a quick script to calculate 2 to the power of 100. You expect a large integer. Go returns Inf. You try to store a 256-bit cryptographic key in an int64 variable. The compiler rejects the code with constant overflows int64. You attempt to add two numbers with 50 decimal places and the result drifts by 0.000000000000001 due to floating-point rounding.

Standard Go types have hard limits. An int64 tops out at roughly 9 quintillion. A float64 gives you about 15 to 17 significant digits of precision. These limits exist because the CPU hardware is optimized for fixed-size arithmetic. The processor has dedicated circuits to add two 64-bit numbers in a single cycle. It does not have circuits to add two numbers that are 10,000 bits long.

When you exceed the hardware limits, you move to software arithmetic. The math package wraps the hardware instructions for speed. The math/big package implements arithmetic algorithms in Go code, using slices of words to represent numbers of arbitrary size. You trade CPU cycles for unlimited range and precision.

Concept in plain words

Think of math as a calculator. It has a fixed display size and fixed memory. It computes instantly, but if the number is too big, it shows an error or switches to scientific notation with limited digits.

Think of math/big as doing long division on a whiteboard. You can write as many digits as you want. The whiteboard is only limited by the size of your room. The trade-off is speed. Writing and erasing digits on a whiteboard takes much longer than pressing a button on a calculator.

The math package provides functions like Sqrt, Sin, Cos, Log, and Pow. These operate on float64 values. They are fast because they map directly to CPU instructions or highly optimized assembly routines.

The math/big package provides types: big.Int for integers, big.Float for floating-point numbers, and big.Rat for rational numbers. These are structs that hold a slice of digits internally. Operations are methods on these structs. There are no operators like + or -. You must call methods like Add, Sub, Mul, and Div.

Minimal example

The math package uses standard function calls. The math/big package uses method calls on value receivers or pointer receivers.

package main

import (
	"fmt"
	"math"
	"math/big"
)

func main() {
	// math.Sqrt uses hardware floating point.
	// It is fast but precision is limited to ~15 decimal digits.
	result := math.Sqrt(2)
	fmt.Println("math.Sqrt(2):", result)

	// big.Int handles arbitrary precision integers.
	// No overflow occurs. The number grows as needed.
	n := new(big.Int)
	n.SetString("1234567890123456789012345678901234567890", 10)
	fmt.Println("big.Int:", n)

	// big.Float handles arbitrary precision floats.
	// You must set precision explicitly for reliable results.
	f := new(big.Float)
	f.SetString("0.123456789012345678901234567890")
	fmt.Println("big.Float:", f.Text('f', -1))
}

Walk through what happens

When you call math.Sqrt(2), the compiler generates a call to the runtime math library. The value 2 is converted to a float64. The CPU executes a square root instruction. The result is returned in a register. This takes nanoseconds.

When you create a big.Int, you allocate a struct on the heap. The struct contains a sign and a slice of uint values representing the digits in base 2^64. When you call n.SetString, the code parses the string character by character, converting groups of digits into binary words and appending them to the slice. If the number is larger than the current slice capacity, the slice grows via reallocation.

The most important difference is mutability. math functions return new values. math/big methods modify the receiver.

// math returns a new value. The input is unchanged.
a := 10.0
b := math.Sqrt(a)
// a is still 10.0

// big.Int modifies the receiver.
x := big.NewInt(10)
y := new(big.Int)
y.Sqrt(x)
// y now holds the square root. x is unchanged.
// The receiver y is the destination of the result.

This design avoids allocating a new big.Int for every operation. It reuses memory. It is efficient but requires you to manage the destination variable explicitly.

Convention aside: receiver naming

Go convention dictates that receiver names be short, usually one or two letters. In math/big, the receiver is almost always the destination of the operation.

The method signature for addition is func (z *Int) Add(x, y *Int) *Int. The receiver z is the result. x and y are the operands. The method returns z to allow chaining.

Do not name the receiver this or self. Use z for the destination, x and y for inputs. This matches the mathematical notation z = x + y. The community expects this pattern. Following it makes your code readable to other Go developers.

Realistic example

A common use case for math/big is handling large identifiers, such as database IDs that exceed 64 bits, or cryptographic values. You often need to parse a string, perform an operation, and format it back.

Here is a function that increments a large integer ID represented as a string. It handles the parsing, the mutation, and the formatting.

package main

import (
	"fmt"
	"math/big"
)

// IncrementID parses a large integer string, adds one, and returns the result.
// It reuses the big.Int allocation to minimize garbage collection pressure.
func IncrementID(idStr string) (string, error) {
	// Parse the string into a big.Int.
	// SetString returns the Int and a boolean indicating success.
	n := new(big.Int)
	_, ok := n.SetString(idStr, 10)
	if !ok {
		return "", fmt.Errorf("invalid integer format: %s", idStr)
	}

	// Create a constant one for addition.
	// big.NewInt creates a new Int with the given value.
	one := big.NewInt(1)

	// Add one to n.
	// The receiver n is modified in place.
	// n.Add(n, one) is equivalent to n = n + 1.
	n.Add(n, one)

	// Convert back to string.
	// String() returns the decimal representation.
	return n.String(), nil
}

func main() {
	id := "99999999999999999999999999999999999999999999999999"
	next, err := IncrementID(id)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Next ID:", next)
}

The SetString method returns a boolean. You must check it. If the string contains non-digit characters, parsing fails. Ignoring the boolean return value is a common bug. The compiler will not warn you if you discard the boolean, but your program will silently produce incorrect results.

Pitfalls and compiler errors

The math/big package trips up developers coming from languages with operator overloading. You cannot use +, -, *, or / with big.Int values.

If you write a + b where a and b are *big.Int, the compiler rejects the code with invalid operation: operator + not defined on *big.Int. You must use a.Add(a, b).

Mutability causes aliasing bugs. If you pass a big.Int to a function, and that function modifies it, the original value changes.

a := big.NewInt(10)
b := a // b points to the same memory as a
b.Add(b, big.NewInt(5))
fmt.Println(a) // Prints 15, not 10

To avoid this, clone the value before modifying it. Use a.Clone() or new(big.Int).Set(a).

big.Float precision is another trap. The precision is specified in bits, not decimal digits. The default precision is 512 bits. If you perform many operations, rounding errors accumulate. You must set the precision explicitly using SetPrec.

f := new(big.Float)
f.SetPrec(256) // Set precision to 256 bits
f.SetString("0.1")

If you forget to set precision, you might get unexpected rounding. The big.Float type also distinguishes between precision and accuracy. Precision is the number of bits in the mantissa. Accuracy is the error bound. The API uses Prec for precision.

big.Rat provides exact rational arithmetic. It stores a numerator and denominator as big.Int values. It avoids floating-point drift entirely.

r := big.NewRat(1, 3)
fmt.Println(r) // Prints 1/3

Use big.Rat when you need exact fractions, such as in financial calculations or symbolic math. It is slower than big.Float but eliminates rounding errors.

Decision: when to use this vs alternatives

Use int or int64 when your values fit within 64 bits and you need maximum performance. Use float64 when you need floating-point math and ~15 digits of precision is sufficient. Use big.Int when you need exact integers larger than 2^63 or smaller than -2^63. Use big.Float when you need decimal numbers with a range or precision exceeding float64. Use big.Rat when you need exact fractional arithmetic without rounding drift. Use plain sequential code when you don't need concurrency: the simplest thing that works is usually the right thing.

Where to go next

Mutability is a feature, not a bug, but it bites. Clone before you modify. Precision is bits, not digits. Set it explicitly. Math is fast. Big is big. Don't mix them up.