1. Introduction
Almost all beginners (and some experienced folks too—especially on Fridays) confuse two things: rounding as a mathematical operation and rounding as an output format. From the outside it looks the same—“I want 2 digits after the decimal point”—but inside these are different actions with different consequences for your program.
Imagine the number is food, and output is the plate. You can cut the food (change the number), or you can arrange it nicely on the plate (change only the display). The second operation doesn’t change the food itself—only how you present it.
In Go:
- formatting via fmt.Printf("%.2f", x) does not change x;
- mathematical rounding via math.Round(x) returns a new value.
This difference is the main point of the lecture.
Formatting does not change the value
It’s very useful once in your life (preferably early) to see with your own eyes that formatting does not change the variable. Otherwise, you can accidentally build your program logic on “pretty output”—which is roughly like building a house from a photo of a brick.
Let’s do a simple check: print the number “nicely”, then print the same number with high precision to see that it did not become “shorter”.
package main
import "fmt"
func main() {
x := 2.0 / 3.0
fmt.Printf("Pretty: %.2f\n", x) // Pretty: 0.67
fmt.Printf("Raw: %.17f\n", x) // Raw: 0.66666666666666663
fmt.Printf("Still x: %.17f\n", x) // Still x: 0.66666666666666663
}
If this example gives you a slight feeling of “I was tricked”—congratulations, you’re starting to understand float. This is not a Go bug and not a mathematicians’ conspiracy. In binary, not all decimal fractions can be represented exactly, so you get tails.
2. Formatting floats in fmt.Printf
When you print a float64, by default Go tries to print it “reasonably”, but that’s not always what a human wants. So fmt.Printf gives you control: how many digits after the decimal point to show, which format to choose, how to print a number in a report, and so on.
What %.2f means
Let’s start with the most popular: %f and its “pretty version” %.2f.
- %f prints a floating-point number in decimal form.
- %.2f prints exactly 2 digits after the decimal point, rounding for display.
- %.0f prints without a fractional part (also rounding for display).
- %.3f prints three digits after the decimal point, and so on.
Let’s look at a simple example.
package main
import "fmt"
func main() {
x := 1.0 / 3.0
fmt.Printf("x as %%f = %f\n", x) // x as %f = 0.333333
fmt.Printf("x as %%.2f = %.2f\n", x) // x as %.2f = 0.33
fmt.Printf("x as %%.0f = %.0f\n", x) // x as %.0f = 0
}
Notice two subtleties.
First: in a format string, %% is how you print a literal % symbol. Otherwise fmt will think you are starting formatting again.
Second: x did not change. It was “one third” (approximately) and it stayed that way. We just displayed it differently.
Other useful formats: %.0f and %g
Once you start printing numbers, the question comes up quickly: “Is %.2f always good?” The answer is no. It’s great for money and “human-friendly” reports, but sometimes you need something else.
The important part is not memorizing all specifiers (that’s a path to sadness), but understanding the idea: the format is chosen to match the meaning of the output.
A small table that is useful to keep in mind:
| Format | Idea | Typical meaning |
|---|---|---|
|
“regular” real number | technical output, debugging |
|
fixed precision after the decimal point | money, percentages, reports |
|
no fractional part | “items”, “people”, “whole units”, but the number is still a float |
|
“compact”: either normal form or exponent | diagnostics when scales vary |
|
significant digits (not “after the decimal point”) | compact reports, scientific numbers |
Let’s try %g in a short example: it often looks “smarter” when numbers can be both small and large.
package main
import "fmt"
func main() {
a := 12345.6789
b := 0.000012345
fmt.Printf("a with %%f = %f\n", a) // a with %f = 12345.678900
fmt.Printf("a with %%g = %g\n", a) // a with %g = 12345.6789
fmt.Printf("b with %%g = %g\n", b) // b with %g = 1.2345e-05
fmt.Printf("a with %%.3g = %.3g\n", a) // a with %.3g = 1.23e+04
}
If you’re making a “human receipt”, %.2f is usually better. If you’re printing diagnostic output, %g can be clearer sometimes because it doesn’t bury you in extra zeros.
Mini app: a tip calculator
Now we’ll assemble a small piece in the style of our course: the program reads input, computes something, and prints the result. No architecture, no classes—just an honest main and understandable variables. This matters: we are practicing the skill “I can build something useful from basic parts”.
Let’s imagine we’re writing a mini tip calculator:
- bill amount (a real number),
- tip percent (an integer, for example 10 or 15),
- final total to pay.
We will print like money: %.2f.
package main
import "fmt"
func main() {
var bill float64
var tipPercent int
fmt.Scan(&bill, &tipPercent)
tip := bill * float64(tipPercent) / 100
total := bill + tip
fmt.Printf("Tip: %.2f\n", tip) // Tip: 3.75 (example)
fmt.Printf("Total: %.2f\n", total) // Total: 28.75 (example)
}
Here we are using several things that “click together” into one skill.
First, fmt.Scan can read float64 and int just fine, as long as the input is valid.
Second, we explicitly convert tipPercent to float64, because otherwise the expression bill * tipPercent won’t compile (the types are different—no magic).
Third, %.2f makes the output look like what a human expects. And yes, this is exactly why %.2f is so popular in money examples: not because “that’s how it’s done”, but because it’s the minimal sensible order.
3. Rounding the value with the math package
Sometimes “pretty output” is not enough. For example, you want to:
- compute a sum and then use a rounded value in subsequent calculations;
- apply a rule like “round up” (for example, “shipping is always rounded up to whole rubles”);
- cut off the fractional part downward and get a “whole number of packages”.
In such cases, you need functions from the math package.
Round, Floor, Ceil
In the basic set, three functions cover most everyday scenarios:
- math.Round(x) — round to the nearest integer;
- math.Floor(x) — round down (toward negative infinity);
- math.Ceil(x) — round up (toward positive infinity).
Let’s look at an example.
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Round(2.5)) // 3
fmt.Println(math.Floor(2.9)) // 2
fmt.Println(math.Ceil(2.1)) // 3
}
This is already a change to the value—more precisely, obtaining a new value. The variable itself doesn’t change (because we didn’t change it), but the function result is a new number that you can assign, add, store, etc.
Rounding to two decimal places as a value: “scale → Round → back”
Formatting with %.2f prints two digits after the decimal point, but it does not change the value itself. But if you want the value with two digits (for example, to store a “discounted amount” already rounded), a common approach is:
- multiply by 100,
- round to an integer via math.Round,
- divide back by 100.
Example:
package main
import (
"fmt"
"math"
)
func main() {
x := 12.3456
y := math.Round(x*100) / 100
fmt.Printf("x = %.10f\n", x) // x = 12.3456000000
fmt.Printf("y = %.10f\n", y) // y = 12.3500000000
}
Why does this work? Because we turn the task “round to 2 digits” into the task “round to an integer”, and math.Round can do that directly.
An important note right away: we are working with float, which means there is no “perfect financial math” here. But at our learning level, this is a normal and understandable tool: you can clearly see the formula and understand what’s going on.
4. Where to do rounding in a program
When a program starts computing something longer than “two plus two”, the place where you round becomes important. If you round too early, you can accumulate error. If you never round at all, it will be painful for a user to look at the report. So it’s useful to mentally separate the stages.
flowchart TD
A[Data input] --> B[Calculations in float64]
B --> C{Do you need a rounded value for further calculations?}
C -->|no| D["Format on output fmt.Printf('%.2f')"]
C -->|yes| E["Round the value< math.Round(x*100)/100"]
E --> D
The idea is simple: if rounding is needed only “to look nice”, do it in Printf. If rounding is part of a business rule (even a very simple one), round the value via math.
By the way, the “make code simpler so you make fewer mistakes” philosophy is not just boring moralizing. In the Go community, it is often stated explicitly as an engineering approach: less unnecessary complexity means fewer chances to “miss a small detail and then spend a week hunting a bug”.
5. float quirks: the 0.1 + 0.2 example
This example has become a meme already, but it’s useful. We won’t dive into deep theory; we’ll just reinforce a practical idea: float is approximate, and formatting can hide tails… but the tails still exist.
package main
import "fmt"
func main() {
x := 0.1 + 0.2
fmt.Printf("x = %.17f\n", x) // x = 0.30000000000000004
fmt.Printf("x = %.2f\n", x) // x = 0.30
}
If you’re making a report for a human, %.2f is fine and even necessary. If you write logic like if x == 0.3 { ... }, you will most likely end up with strange results (but comparison strategies with a tolerance are a separate discipline, and we are intentionally not going there right now).
6. Typical mistakes
Mistake #1: “I printed %.2f, so the number became two-decimal”.
This is the most common confusion. Formatting is only a way to display a value. The variable won’t change, even if the output looks perfect. If later in the code you use the original x, then you are using the original x with all its “tails”. If you need the rounded value itself, you must compute it separately and store it in a variable.
Mistake #2: trying to round using int(x) instead of math.Round.
Converting a float64 to int drops the fractional part, i.e., truncates toward zero. For 3.9 that’s 3, and for -3.9 that’s -3. This is not “human-style” rounding; it’s a different operation. As a result, you can get errors that look like “sometimes a cent less”, which is a classic road to eternal sadness.
Mistake #3: printing money using %v or %f without precision.
Technically, the program works, but the UX does not. Users are not obligated to love your six digits after the decimal point. Money and percentages are almost always expected in %.2f format (or another fixed one); otherwise the output is perceived as “raw internals” rather than a result.
Mistake #4: “First I’ll round every tiny piece, then add it up—so it’s more accurate”.
It feels intuitive, but it often produces more accumulated error: rounding “eats” information. At a basic level, a useful rule is: if rounding is only for display, round at the very end (via Printf). If rounding is part of the rule, do it exactly where the rule should take effect, and don’t smear rounding all over the code.
Mistake #5: forgetting to import "math" and arguing with the compiler for a long time.
The Go compiler is usually quite honest: if you write math.Round, but you forgot import "math", it won’t “guess” for you. It’s annoying for about 20 seconds, but then you get used to the fact that dependencies in code are visible explicitly. And yes, that’s also part of that same “simplicity that reduces surprises”.
GO TO FULL VERSION