Right, let’s talk about numbers that can’t make up their mind: floating-point numbers. You need them for almost anything interesting—graphics, simulations, science, even just dividing 10 by 3. They’re called “floating-point” because the decimal point can float; the number of digits before and after it isn’t fixed. Go gives you two main flavors: float32 (single-precision) and float64 (double-precision). Unless you’re in a memory-constrained environment (like embedded systems) or working with a specific API that demands them, you should almost always use float64. It’s the default for most literals and it’s what the math package expects. The extra precision and range are worth the trivial memory cost on modern hardware.

The Inevitable Precision Problem

Here’s the first thing you need to get through your head: float32 and float64 are approximations. They can’t perfectly represent most decimal fractions, just like you can’t perfectly write out 1/3 (0.333333…) in base-10 with a finite number of digits. Computers use base-2, and the same problem exists. This leads to some truly “fun” results.

package main

import "fmt"

func main() {
    // Seems simple, right?
    a := 0.1
    b := 0.2
    c := 0.3

    fmt.Println(a + b)         // Output: 0.3
    fmt.Println(a + b == c)    // Output: false. Wait, what?
    fmt.Printf("%.20f\n", a+b) // Output: 0.30000000000000004441
    fmt.Printf("%.20f\n", c)   // Output: 0.29999999999999998890
}

See that? 0.1 + 0.2 != 0.3. This isn’t a bug in Go; it’s a fundamental property of IEEE 754 floating-point arithmetic that every programmer must understand. The numbers 0.1 and 0.2 are infinite repeating fractions in base-2, so they get rounded to the nearest value that can be represented. The tiny errors from this rounding accumulate. This is why you never, ever use == or != to compare floating-point numbers for equality. It’s a direct path to madness.

How to Actually Compare Floats

So if == is off the table, what do you do? You check if two numbers are close enough to each other, within a certain tolerance. This tolerance is often called an epsilon value. The math package provides a useful starting point with math.Nextafter. The trick is to use a relative tolerance, which scales with the magnitude of the numbers you’re comparing.

package main

import (
    "fmt"
    "math"
)

// approximatelyEqual compares two floats with a relative tolerance.
// It's a good starting point for most use cases.
func approximatelyEqual(a, b, tolerance float64) bool {
    return math.Abs(a-b) <= tolerance * math.Max(math.Abs(a), math.Abs(b))
}

func main() {
    a := 0.1 + 0.2
    b := 0.3

    fmt.Println(approximatelyEqual(a, b, 1e-12)) // Output: true
    fmt.Println(approximatelyEqual(a, b, 1e-17)) // Output: false (too strict!)
}

For most practical purposes, a tolerance between 1e-8 and 1e-12 is reasonable. If you’re working with very large or very small numbers, you might need to adjust this. The standard library doesn’t provide a single function for this because the “right” tolerance is deeply application-specific.

Special Values: NaN and Inf

Floating-point numbers have their own version of existential crises: they can represent “Not a Number” (NaN) and infinity (±Inf). These aren’t just theoretical; they show up when you do things that break math.

package main

import (
    "fmt"
    "math"
)

func main() {
    // Generating our special friends
    nan := math.Log(-1.0) // Take log of a negative number
    posInf := 1.0 / 0.0   // Division by zero
    negInf := -1.0 / 0.0  // Division by zero

    fmt.Println(nan, posInf, negInf) // Output: NaN +Inf -Inf

    // How to check for them? Use the math package functions.
    fmt.Println(math.IsNaN(nan))    // Output: true
    fmt.Println(math.IsInf(posInf, 1))  // Output: true (positive infinity)
    fmt.Println(math.IsInf(negInf, -1)) // Output: true (negative infinity)

    // Here's the kicker: NaN is not equal to itself. By definition.
    fmt.Println(nan == nan) // Output: false. Always.
}

This is why you must use math.IsNaN() to check for NaN. Any other comparison, including ==, will fail. It’s the only thing in Go that isn’t equal to itself, which is a fantastic party trick and a constant source of bugs.

The Zero Value and Defaults

The zero value for any float type is 0.0 (or just 0). And remember, var f float32 will give you 0.0, a perfectly valid number. Be cautious, because this can be a source of subtle bugs if you’re expecting an uninitialized variable to behave differently.

When you write a floating-point literal like 3.14, Go interprets it as a float64. If you need a float32, you must explicitly convert it: float32(3.14). This conversion from a higher-precision type to a lower one truncates the extra bits, which can lead to precision loss. It’s a narrowing conversion, so do it with intention, not by accident.

The bottom line? Use float64. Expect and plan for tiny rounding errors. Never use ==. Check for NaN and Inf with the math functions. It’s a messy, imperfect system, but it’s the best we’ve got for representing the continuous reality of math in the discrete world of computers.