Let’s be honest: you’re not here because you love math. You’re here because you need to get numbers to behave. Go’s math package is your no-nonsense Swiss Army knife for this. It’s not a symbolic math library; it’s a collection of fast, precise functions for common (and some uncommon) operations. It’s the kind of friend who will tell you that 0.1 + 0.2 doesn’t equal 0.3 in floating-point land and then hand you the right tool to deal with it.

The Basics: Constants and Elementary Functions

First, some freebies. The package provides constants you’d otherwise have to Google, like math.Pi and math.E. Use them. Don’t define your own const pi = 3.14159 unless you want a subtle bug in the 8th decimal place to ruin your day.

The elementary functions are all here: math.Sqrt, math.Pow(x, y) for xʸ, math.Log, math.Max, math.Min, math.Abs, and all the trig functions. They do exactly what you’d expect, with one critical caveat: they operate on float64. This is a conscious design choice. Go’s philosophy is to provide one, well-implemented, precise floating-point type for these operations. If you have a float32, you convert it. It feels a bit clunky, but it avoids a combinatorial explosion of function names (Sqrt, Sqrt32, Sqrt64) and ensures you’re getting the best precision available.

// Calculating the hypotenuse of a right triangle
a := 3.0
b := 4.0
c := math.Sqrt(math.Pow(a, 2) + math.Pow(b, 2))
fmt.Printf("%.1f\n", c) // Output: 5.0

// But wait, that's inefficient. Pow is overkill for squaring.
// This is better and avoids unnecessary function calls:
c = math.Sqrt(a*a + b*b)

The Floating-Point Minefield

Here’s where we get to the “brilliant friend” part. Floating-point arithmetic is, by its very nature, an approximation. The math package doesn’t hide this; it gives you the tools to navigate it safely.

Never, ever use == or != with floating-point numbers. The results are unpredictable. Instead, you compare within a tolerance. The math package helps by providing math.Float64bits to get the raw bits, but what you’ll use most often is a simple absolute or relative tolerance comparison.

// The classic 0.1 + 0.2 problem
result := 0.1 + 0.2
fmt.Println(result == 0.3) // Output: false. Told you so.
fmt.Printf("%.18f\n", result) // Output: 0.300000000000000044

// The right way: compare with a tolerance.
tolerance := 1e-10
isEqual := math.Abs(result - 0.3) <= tolerance
fmt.Println(isEqual) // Output: true

For a more robust method, especially when dealing with very large or small numbers, you might use a relative tolerance. This is where you’d write a helper function because, curiously, the standard library doesn’t include one. A common implementation checks that the absolute difference is within a small fraction of the larger of the two values.

When float64 Isn’t Enough: Introducing math/big

Sometimes, your numbers are too big, too precise, or too… whole for float64. Maybe you’re calculating interplanetary budgets or dealing with cryptography. This is where math/big marches in, wearing a utility belt and a grim expression.

math/big gives you three types: Int for integers, Rat for rational numbers (fractions), and Float for floating-point with arbitrary precision. They are more complex to use because they are reference types with their own methods for operations. You don’t write z = x + y; you write z.Add(x, y). This feels archaic but it’s for a good reason: it lets you control memory allocation by reusing objects.

// Calculating 2^100 the right way (without overflowing an int)
bigPower := new(big.Int)
bigPower.Exp(big.NewInt(2), big.NewInt(100), nil) // nil means no modulus
fmt.Println(bigPower) // Output: 1267650600228229401496703205376

// Let's do some precise fractional math
a := big.NewRat(1, 3) // 1/3
b := big.NewRat(1, 6) // 1/6
sum := new(big.Rat)
sum.Add(a, b)
fmt.Println(sum.FloatString(3)) // Output: 0.500

The biggest pitfall with math/big is forgetting that operations are methods that modify the receiver. This code is wrong:

x := big.NewInt(10)
y := big.NewInt(20)
z := x.Add(x, y) // DON'T DO THIS (unless you intend to modify x)
fmt.Println(x, z) // x is now 30! Both print "30".

The correct way is to use a separate Int as the receiver to avoid aliasing:

x := big.NewInt(10)
y := big.NewInt(20)
z := new(big.Int)
z.Add(x, y) // z = x + y, x remains 10
fmt.Println(x, z) // Output: 10 30

Special Values and Checking Results

The math package also provides constants for the special values your floating-point operations can return: positive and negative infinity (math.Inf), NaN (“Not a Number”) (math.NaN), and Max/Min values. You must check for these. Dividing by zero gives you Inf. Taking the square root of a negative number gives you NaN. These values propagate through your calculations and silently destroy your logic.

// How to check for these nuisances
value := math.Log(-1) // This is not a number
fmt.Println(value, math.IsNaN(value)) // Output: NaN true

value = math.Exp(1000) // This is probably too big
fmt.Println(value, math.IsInf(value, 0)) // Output: +Inf true

In summary, math is your efficient, sharp tool for everyday number crunching, while math/big is the heavy machinery you call in for the big jobs. Respect the limitations of floating-point, always check your edge cases, and remember: math/big doesn’t want to share memory. It’s a bit of a diva that way.