1. Getting to know float
If you’ve only gotten used to int, then floating-point numbers (like “3.14”, “2.5”, “0.1”) may seem simple at first: just a fraction. But in programming they’re like a person who promised to show up “exactly at 19:00” and then arrives “around seven, but actually at eight.” In other words, floating-point numbers on a computer are almost always approximate.
In Go, floating-point numbers are represented by float32 and float64. You need them when working with measurements, averages, percentages, money as “euros and cents” (although for money people often use integer cents), coordinates, time in seconds with a fractional part, and so on.
The problem is that the computer stores such numbers not in decimal, but in binary, and some familiar decimal fractions cannot be represented “exactly” in binary. That’s why you occasionally see “tails” and oddities in comparisons.
2. float32 and float64: differences and what is chosen by default
When you see float32 and float64, the most important thing is not to memorize “millions of digits after the decimal point,” but to understand the idea: these are floating-point numbers, and they have limited precision that depends on the number of bits.
float32 takes 32 bits (4 bytes), and float64 takes 64 bits (8 bytes). The more bits, the higher the precision (and usually the wider the range). In real-world programming, float64 is used more often because on modern computers the memory difference is rarely critical, while the precision difference definitely is.
A small cheat sheet:
| Type | Size | Approximate precision (in decimal digits) | “Default” for the literal 0.1 |
|---|---|---|---|
|
4 bytes | ~6–7 significant digits | no |
|
8 bytes | ~15–16 significant digits | yes |
In Go, untyped numeric literals (for example, 0.1, 2.5, 1.0/3.0) become float64 by default when you assign them to a variable without an explicit type. This is usually convenient: more precision, fewer surprises.
Mini example: let’s look at the literal’s type via %T (diagnostic printing is our programmer’s “magnifying glass”):
package main
import "fmt"
func main() {
x := 0.1
var y float32 = 0.1
fmt.Printf("x=%v, type=%T\n", x, x) // x=0.1, type=float64
fmt.Printf("y=%v, type=%T\n", y, y) // y=0.1, type=float32
}
3. The 0.1 + 0.2 effect and comparing floats
Why 0.1 + 0.2 might not equal 0.3
Now comes the main “magic trick” that breaks almost everyone’s brain the first time (and that’s normal).
Math says:
- 0.1 + 0.2 = 0.3
The computer sometimes says:
- 0.1 + 0.2 = 0.30000000000000004
And you’re like: “Go, are you serious?” And it is serious. Go is just honest.
Reason: fractions like 0.1 are infinite in binary (like 1/3 in decimal: 0.333333...). So the number is stored approximately, and then arithmetic works with that approximation.
Let’s write a minimal example and print the result with high precision. The key point: we’re not “going crazy because of extra digits,” we’re using them for diagnostics.
package main
import "fmt"
func main() {
a := 0.1
b := 0.2
c := a + b
fmt.Printf("c = %.17f\n", c) // c = 0.30000000000000004
}
Yes, it looks like a bug, but this is expected behavior for floating-point numbers.
To build intuition, imagine the number’s path inside the computer as a sequence of steps:
flowchart LR
A["Decimal fraction<br/>0.1"] --> B["Binary representation<br/>(infinite fraction)"]
B --> C["Rounding to float64<br/>(limited precision)"]
C --> D["Arithmetic<br/>+ 0.2"]
D --> E["Printing the result<br/>(we see a tail)"]
Even == can betray you at the worst time
In the world of int, comparison with == almost always behaves the way you expect. In the world of floats, be more careful.
When you write:
x := 0.1 + 0.2
y := 0.3
fmt.Println(x == y)
you expect true. But in practice it’s easy to get false, because x and y may differ by a microscopic amount, and == requires an exact match of the bit representation.
Mini example:
package main
import "fmt"
func main() {
x := 0.1 + 0.2
y := 0.3
fmt.Println(x) // 0.30000000000000004 (often)
fmt.Println(y) // 0.3
fmt.Println(x == y) // false (often)
}
What should you do about it? At the level of this lecture, it’s enough to remember a practical rule: if a float is produced by computations, comparing it with == may be inaccurate.
In serious tasks, people usually compare “with a tolerance,” i.e., they check that the numbers are close enough. But that’s a separate topic, and we won’t turn today’s lecture into a numerical methods course.
4. Computation errors and a bit of practice
float32 vs float64 using 1.0/3.0 as an example
With “nice” decimal fractions, everything sometimes looks fine, but once you take a number that definitely can’t be represented in a finite form, such as 1/3, the precision difference becomes more noticeable.
package main
import "fmt"
func main() {
var f32 float32 = 1.0 / 3.0
var f64 float64 = 1.0 / 3.0
fmt.Printf("f32 = %.10f\n", f32) // f32 = 0.3333333433
fmt.Printf("f64 = %.10f\n", f64) // f64 = 0.3333333333
}
Notice that float32 is “more off.” This isn’t “bad Go,” it’s simply fewer bits for precision.
Accumulation of errors across many operations
There’s one more effect that’s useful to see: even if each individual error is small, with many operations it can “accumulate.”
For example, let’s add 0.1 10 times and see whether we get 1.0:
package main
import "fmt"
func main() {
sum := 0.0
for i := 0; i < 10; i++ {
sum += 0.1
}
fmt.Printf("sum = %.17f\n", sum) // sum = 0.99999999999999989 (often)
}
Mathematically, we should get exactly 1.0, but in the binary world we added approximate values 10 times. So the final result may be slightly smaller or larger than expected.
Important: this doesn’t mean floats “can’t be used.” It means you need to understand their nature and not build business logic on “perfect decimal fraction precision.”
Mini‑practice: computing an average
Let’s continue our learning style of “read → compute → print” and build a mini program that computes an average. This is a simple and realistic case: average temperature, average score, average speed.
Suppose the user enters two numbers with a fractional part (for example, two scores or two temperatures). We read them as float64, compute the average, and print it.
package main
import (
"fmt"
)
func main() {
var a float64
var b float64
fmt.Scan(&a, &b) // input: 0.1 0.2
avg := (a + b) / 2
fmt.Printf("avg = %.17f\n", avg) // avg = 0.15000000000000002 (possible tail)
}
Even the average of 0.1 and 0.2 can sometimes produce a “tail.” And this is a great moment: now you don’t panic—you understand where it comes from.
How to debug floats without getting depressed
When you see a strange number, you either want to “round the output and forget it,” or blame the compiler. But the best approach is to switch into engineer mode: “I’m going to find out what is actually stored.”
For that, it’s handy to:
- print many digits after the decimal point (for example, %.17f for float64);
- print the value’s type with %T if you’re not sure whether you accidentally ended up with float32;
- sometimes print with plain %v to see a “human-friendly” view.
A small “diagnostics” example:
package main
import "fmt"
func main() {
x := 0.1 + 0.2
fmt.Printf("x=%v\n", x) // x=0.30000000000000004 (often)
fmt.Printf("x=%.17f\n", x) // x=0.30000000000000004
fmt.Printf("type=%T\n", x) // type=float64
}
5. Common mistakes when working with floats
Mistake #1: expecting “perfect decimal math.”
Beginners often think: “I typed 0.1, so inside it’s 0.1.” But what’s stored is the nearest representable binary value. The usual symptom: output shows tails like ...0000004 or ...9999998. The cure is not a “fmt spell,” but understanding the nature of floats.
Mistake #2: using float32 by default for no reason.
Sometimes it seems: “float32 is smaller, so it’s better.” But in most learning and practical tasks, float64 gives more predictable results. Symptom: precision “falls apart” too early, especially after several operations or division.
Mistake #3: comparing computed results with == and building logic on it.
A classic. Symptom: “sometimes the condition triggers, sometimes it doesn’t,” especially if your code sums fractions. With int that almost always means a logic bug, but with floats it can also be a precision issue. A useful habit: if you really want ==, first ask yourself where the number came from—“input as a fixed value” or “computed.”
Mistake #4: printing floats without understanding that formatting is only presentation.
If you print %.2f, you make the number look nice for a human, but you do not change what is stored in the variable. Symptom: on the screen “everything is exactly 0.30,” but comparison/summing logic still behaves oddly. It’s important to separate “the value in memory” from “how we display it.”
Mistake #5: being surprised that repeated addition gives an unexpected result.
Adding 0.1 ten times is one of the most famous examples of accumulated error. Symptom: instead of 1.0 you see 0.999999999999.... This does not mean the loop is wrong. It means you’ve run into the reality of floating-point representation.
GO TO FULL VERSION