1. Why do we need fmt.Printf if we have fmt.Println?
When you are just starting to program, fmt.Println feels perfect: it prints values, inserts spaces by itself, and adds a newline by itself. But at the exact moment you try to understand “why it doesn’t work the way I expect,” Println becomes too “kind” and hides some details. fmt.Printf is the “I’m an adult, I want to control the output myself” mode: where the spaces go, where the newline goes, how exactly to print the value, and what its type is.
Imagine Println as a cafe where you get “combo #1,” and Printf as a builder: you pick the bun, the patty, the sauce, and even how loudly the waiter says your order. Sometimes the “combo” is enough. But for debugging and neat output, the builder is more often what you want.
A small comparison:
| Function | What it does | Newline | When it’s handy |
|---|---|---|---|
|
Prints “as is” | No | Short bits of text, without “prettiness” |
|
Prints and adds spaces between arguments | Yes | Quick output “just to see it” |
|
Prints using a template (a format string) | No (usually you need \n) | Neat output and diagnosis of types/values |
2. The format string and specifiers
For fmt.Printf to be useful, you need to understand one simple idea: Printf has a format string—this is plain text with special “slots” inside it for substituting values. These “slots” are called specifiers (or format verbs) and look like %....
An important technical fact that is useful here as a “reference point”: the signature of fmt.Printf looks like this—the first argument is the format string, and then any number of values.
So the idea is: template first, data second.
It’s convenient to imagine Printf working like this:
flowchart LR
A["Format string: 'x=%d type=%T '"] --> C["fmt.Printf"]
B["Arguments: x, x"] --> C["fmt.Printf"]
C --> D["stdout: x=10 type=int"]
And now the most important rule: the number of specifiers in the format string must match the number of passed values. Otherwise you will get either %!(EXTRA …) in the output, or %!(MISSING)—and it will look like a curse from ancient gods.
3. Core specifiers and the newline
%v: the universal “show the value in some reasonable way”
%v is the specifier that saves your life when you don’t want to think about what type you are printing. It stands for “value” and prints the value in its default representation.
When you are debugging a program, most of the time you just need to see what is currently in a variable. Not pretty, not in a table, not in a report—just “what’s in there?” That’s where %v is your best friend.
Mini example:
package main
import "fmt"
func main() {
x := 10
fmt.Printf("x=%v\n", x) // x=10
}
Notice: Printf does not add a newline by itself. That’s why \n at the end is not “for looks,” but a way to protect your sanity so the next output doesn’t stick to this one.
Another example with several types:
package main
import "fmt"
func main() {
name := "Gopher"
ok := true
fmt.Printf("name=%v ok=%v\n", name, ok) // name=Gopher ok=true
}
Here %v is good because it treats strings, booleans, and numbers equally calmly. For diagnostics, it’s ideal.
%T: print the type and stop guessing
%T prints the type of the value you passed to Printf. This is super useful when you use := and rely on type inference, and then wonder why an expression doesn’t compile or why a comparison is “not what you expected.”
In Go documentation and materials, the combination %v + %T is often shown specifically as a way to see both the value and its type at the same time.
It’s basically an “X-ray for variables.”
Example “value + type”:
package main
import "fmt"
func main() {
x := 10
fmt.Printf("x=%v type=%T\n", x, x) // x=10 type=int
}
The point is very practical: you start thinking like the Go compiler. Not “well, it’s a number,” but “this is an int.” And the magical thinking gradually goes away.
Mini detective story: why the type can be surprising at all
Sometimes the type is surprising because of literals and “default types.” For example, a string literal without an explicit type becomes string, an integer literal becomes int, and a floating-point literal is usually float64. In one of the discussions about constants, it is explained that when you pass implicitly typed (untyped) values into Printf, they acquire a “default” concrete type—and %T lets you see that.
We won’t dive into constants today, but as a practical trick this is useful: if you’re not sure, print %T.
%d and %s: strict mode for neat output
%v is universal. But if you are printing data for a user (or at least for yourself a week later), sometimes it’s useful to turn on “strict mode”: print numbers as numbers and strings as strings. This makes the output neater and helps you catch mistakes faster.
- %d is for integers (usually int).
- %s is for strings (string).
Example: printing a simple expression “like in a textbook”:
package main
import "fmt"
func main() {
a := 2
b := 3
fmt.Printf("%d + %d = %d\n", a, b, a+b) // 2 + 3 = 5
}
Example: a string template for a greeting:
package main
import "fmt"
func main() {
name := "Anna"
fmt.Printf("Hello, %s!\n", name) // Hello, Anna!
}
This output immediately looks like a “program,” not like a “pile of debug prints.”
\n: the newline is your responsibility
In fmt.Println, a newline is added automatically. In fmt.Printf, it is not. This is not a bug and not “why Go is so strict,” it’s just the principle: if you control the format, you control everything, including line breaks.
So if you want line-by-line output, most often you put \n at the end of the format string.
Bad example (the output will run together):
package main
import "fmt"
func main() {
fmt.Printf("first")
fmt.Printf("second")
fmt.Printf("third")
// firstsecondthird
}
Normal example:
package main
import "fmt"
func main() {
fmt.Printf("first\n") // first
fmt.Printf("second\n") // second
fmt.Printf("third\n") // third
}
From this point on, you can consider yourself officially a person who controls newlines.
4. Example: TripBudget
Let’s evolve a simple learning app called TripBudget: a trip budget calculator. We are not using arrays/slices/functions yet and we are not building a complex architecture. We just read input, calculate, and print the result neatly. At this level, it’s more important to learn to see types and values than to “build a system.”
Version 0: works fast
Let’s say we have these variables: a city, a number of days, and a per-day budget (in whole dollars). We want to calculate the total.
package main
import "fmt"
func main() {
city := "Berlin"
days := 4
perDay := 70
total := days * perDay
fmt.Println(city, days, perDay, total) // Berlin 4 70 280
}
This works, but the output is “raw.” The user can’t tell what is what.
Version 1: output for humans
package main
import "fmt"
func main() {
city := "Berlin"
days := 4
perDay := 70
total := days * perDay
fmt.Printf("Trip to %s: %d days, $%d/day, total=$%d\n", city, days, perDay, total)
// Trip to Berlin: 4 days, $70/day, total=$280
}
Now the output reads like a normal message, not like a “memory dump.”
Version 2: diagnostic mode
Sometimes you change code, move variables around, add new ones—and suddenly you start doubting: “is days really an int?”, “is city really a string?” That’s when we add %T.
package main
import "fmt"
func main() {
city := "Berlin"
days := 4
perDay := 70
fmt.Printf("city=%v type=%T\n", city, city) // city=Berlin type=string
fmt.Printf("days=%v type=%T\n", days, days) // days=4 type=int
fmt.Printf("perDay=%v type=%T\n", perDay, perDay) // perDay=70 type=int
}
This is especially useful when you actively use :=, because the type is inferred automatically, and sometimes you surprise yourself.
5. Printf patterns that save time
Now we’ll cover a few patterns that are worth memorizing by muscle memory. I’m intentionally keeping them short so they are easy to repeat in your head.
Show everything on one line
When you are debugging a condition or a computation, it’s convenient to print everything at once:
package main
import "fmt"
func main() {
a := 10
b := 3
fmt.Printf("a=%d b=%d a/b=%d a%%b=%d\n", a, b, a/b, a%b)
// a=10 b=3 a/b=3 a%b=1
}
Notice a%%b: to print the % character in the format string, you write %%. This is not a new “formatting topic,” just a small everyday detail: % in a format string is a special character.
Check that an expression is really bool
It’s very useful to print not only numbers, but also boolean expressions:
package main
import "fmt"
func main() {
age := 17
canEnter := age >= 18
fmt.Printf("age=%d canEnter=%v type(canEnter)=%T\n", age, canEnter, canEnter)
// age=17 canEnter=false type(canEnter)=bool
}
This helps you stop mixing up “a value” and “a condition.” In Go, a condition must be bool, and %T reinforces that well in practice.
Passed the wrong thing—see it immediately
Sometimes you accidentally print a variable with the wrong specifier. For example, a string with %d. The compiler/runtime will often show you weird output. That’s a good signal: “I’m printing something other than what I think I am.”
A good habit: if the output looks strange, add %T next to it and see what you are actually printing.
6. Common mistakes when working with fmt.Printf
Error #1: you forgot \n and got “glued together” output.
This is a classic: you print several lines in a row, expect neat messages, and they turn into one long “sausage.” The reason is simple: Printf does not add a newline automatically. If you want line-by-line output, \n is your responsibility.
Error #2: the number of specifiers and the number of arguments don’t match.
When the format string has three “slots” (%d %d %d), but you pass two values or four, the output starts looking like a message from aliens. At that point, don’t panic: just reread the format string and count how many substitutions you actually made.
Error #3: mixing up %d and %s.
This mistake is especially painful for beginners, because “well, it looks like text/number.” But Go is a language that respects types. %d is for integers, %s is for strings. If you’re not sure what type you have, add %T and stop guessing.
Error #4: using %v everywhere and losing readability.
%v is great for diagnostics, but if you are producing “human-friendly” output, %d and %s often read better: they discipline the format—and you. A good balance is: for debugging, use %v/%T; for user-facing messages, use stricter %d/%s.
Error #5: trying to make Printf “like Println” and being surprised by spaces.
Println inserts spaces between arguments by itself. Printf does not: spaces, commas, colons, and other punctuation are part of the format string. That is, if you didn’t write a space, it won’t be there. It’s annoying at first, then it turns into a superpower: you start printing exactly the way you intended.
GO TO FULL VERSION