What Is the Difference Between float32 and float64 in Go

float32 is a 32-bit floating-point type for lower precision, while float64 is a 64-bit type offering higher precision and range.

The precision trap

You are building a physics engine for a 2D game. You store positions, velocities, and forces in a slice. The simulation runs fine for a few minutes. Then objects start jittering. Trajectories drift. You check the math. The formulas are correct. The problem is the type you picked to hold the numbers. You chose float32 to save memory, but the accumulated rounding errors broke the simulation. Or you picked float64 everywhere, and your memory usage doubled for no reason because the game only needs rough estimates for particle effects.

Go gives you two floating-point types. float32 uses 32 bits. float64 uses 64 bits. Both follow the IEEE 754 standard. The difference is not just size. It changes how the CPU calculates, how much RAM your program eats, and how accurately it represents the real world.

How floating-point numbers actually work

Computers do not store decimals the way humans write them. They use a binary version of scientific notation. Every IEEE 754 float splits its bits into three parts: a sign bit, an exponent, and a mantissa. The sign bit says positive or negative. The exponent determines the scale. The mantissa holds the actual digits.

float32 allocates 1 bit for sign, 8 bits for exponent, and 23 bits for mantissa. float64 uses 1 bit for sign, 11 bits for exponent, and 52 bits for mantissa. More mantissa bits mean more precision. More exponent bits mean a wider range of values you can represent without overflowing to infinity or underflowing to zero.

Think of it like two different rulers. The 32-bit ruler has marks every millimeter. It fits in your pocket and weighs almost nothing. The 64-bit ruler has marks every fraction of a millimeter. It is longer, heavier, and takes up more desk space. If you are measuring a room, the small ruler is fine. If you are calibrating a microscope, you need the big one.

Pick the ruler that matches the measurement. Extra precision costs memory. Missing precision costs correctness.

The minimal example

Go treats untyped numeric literals as flexible until you assign them to a variable. The language defaults to float64 when you write a decimal number without a type annotation.

package main

import "fmt"

func main() {
    // Untyped literal defaults to float64 when assigned
    // The compiler preserves all digits until storage is fixed
    var defaultFloat = 3.141592653589793
    fmt.Printf("%T\n", defaultFloat)

    // Explicit type forces the compiler to truncate precision
    // Bits beyond the 23-bit mantissa are dropped silently
    var smallFloat float32 = 3.141592653589793
    fmt.Printf("%.20f\n", smallFloat)

    // Converting between the two requires an explicit cast
    // Widening does not restore lost precision, it just pads zeros
    precise := float64(smallFloat)
    fmt.Printf("%T\n", precise)
}

The first variable keeps all the digits. The second variable drops them the moment you assign it to float32. The compiler does not warn you about precision loss during assignment. It assumes you know what you are doing. The third line shows how to widen the type back to 64 bits. The value does not magically regain the lost digits. It just pads with zeros.

What happens under the hood

When you compile a Go program, the compiler maps float32 to the CPU's single-precision registers and float64 to double-precision registers. Modern x86 and ARM processors handle both, but they do not treat them equally.

A float32 operation typically runs in a single cycle on modern hardware. It occupies 4 bytes in memory. A float64 operation takes slightly longer on some architectures and occupies 8 bytes. The real cost shows up when you scale. A slice of one million float32 values takes 4 megabytes. The same slice of float64 values takes 8 megabytes. That extra memory pushes out of the CPU cache faster. Cache misses slow down your program more than the extra CPU cycles ever will.

Go also enforces strict typing. You cannot mix float32 and float64 in arithmetic without an explicit conversion. The compiler rejects mixed operations with an error like invalid operation: operator + not defined on a (variable of type float32) and b (variable of type float64). This strictness prevents silent precision loss. You have to write float64(a) + b or float32(a + float32(b)). The verbosity is intentional. It forces you to decide where precision matters.

The standard library follows this rule too. The math package operates on float64. Go 1.20 introduced math32 for float32 operations. The community convention is to import the package that matches your type. You avoid constant casting and keep the intent obvious.

The compiler enforces the boundary. You decide where it goes.

A realistic scenario

Imagine you are processing sensor data from a fleet of IoT devices. Each device sends temperature, humidity, and pressure readings every second. You store the readings in a struct and append them to a slice.

// Reading holds a single sensor snapshot
// Using float32 keeps the struct small for high-throughput pipelines
type Reading struct {
    Timestamp int64
    Temp      float32
    Humidity  float32
    Pressure  float32
}

func main() {
    // Preallocate to avoid repeated allocations during streaming
    buffer := make([]Reading, 0, 1000)

    // Simulate incoming data from the network
    for i := 0; i < 1000; i++ {
        buffer = append(buffer, Reading{
            Timestamp: int64(i),
            Temp:      22.5,
            Humidity:  45.2,
            Pressure:  1013.25,
        })
    }

    // Calculate average temperature
    // Accumulating in float32 matches the source precision
    var sum float32
    for _, r := range buffer {
        sum += r.Temp
    }
    avg := sum / float32(len(buffer))
    fmt.Printf("Average: %.2f\n", avg)
}

The struct uses float32 because environmental sensors rarely need more than two decimal places of accuracy. The memory footprint stays low, which matters when you are streaming thousands of readings per second. If you switched to float64, the struct would grow from 24 bytes to 32 bytes. That extra 8 bytes per record multiplies quickly. Network payloads get larger. Database rows get wider. Garbage collection runs more often.

Now imagine you are calculating orbital mechanics for a satellite. You need float64. The exponent range prevents overflow when tracking distances in kilometers. The mantissa keeps the trajectory stable over millions of iterations. Picking the wrong type here does not just waste memory. It makes the simulation physically incorrect.

Memory is cheap until it is not. Cache misses are expensive. Size your types to the data, not to habit.

Pitfalls and compiler behavior

Floating-point math breaks intuition. The number 0.1 cannot be represented exactly in binary. It becomes an infinitely repeating fraction. When you add 0.1 ten times, you do not get exactly 1.0. You get 0.9999999999999999 or 1.0000000000000002 depending on the type and the CPU.

Comparing floats with == is dangerous. Two calculations that should yield the same result often differ by a tiny epsilon. The compiler will not stop you from writing if a == b. It will compile fine. The bug shows up at runtime when your condition fails unexpectedly. Use a tolerance check instead. Subtract the values, take the absolute difference, and compare against a small threshold.

Go also has special floating-point values. math.NaN() represents an invalid result like 0.0 / 0.0. math.Inf(1) represents positive infinity. math.Inf(-1) represents negative infinity. These values break sorting and hashing. A slice containing NaN will not sort correctly because NaN is not equal to itself. The compiler does not catch this. You have to handle it explicitly.

Another common mistake is assuming float32 is faster in Go. The Go compiler and runtime optimize heavily. On 64-bit systems, the CPU often loads float32 values into 64-bit registers anyway. The performance difference is usually negligible unless you are processing millions of values in a tight loop. Memory bandwidth and cache locality matter far more than register width.

Floats are approximations. Treat them like measurements, not exact values.

When to pick which type

Use float32 when you are working with graphics, audio samples, or sensor data where human perception caps the useful precision. Use float32 when memory bandwidth or cache size is the bottleneck in a tight loop. Use float32 when you are sending data over a network and every byte counts.

Use float64 when you are doing scientific computing, financial modeling, or physics simulations where accumulated rounding errors will corrupt the result. Use float64 when you need to represent very large or very small numbers without hitting overflow or underflow. Use float64 as the default for general-purpose math. The extra memory cost is rarely worth the debugging time spent chasing precision bugs.

Use explicit conversions when you must bridge the two types. Never rely on implicit coercion. The compiler will reject mixed arithmetic, which is a feature, not a bug. Write the cast where the precision boundary actually exists.

Where to go next