Formatting and Printing (fmt)
The fmt package is Go’s standard toolkit for formatted input and output — printing values to the terminal, building strings, writing to files or network connections, and parsing text back into typed values. Almost every Go program uses it, from a one-line “hello world” to structured logging in a production service. Understanding fmt well means understanding Go’s approach to formatting: a small, consistent set of verbs, a sensible default representation for every type, and an interface — Stringer — that lets your own types plug into that machinery.
Overview / How fmt Works
fmt is organized into three families of output functions, each producing the same text but sending it somewhere different:
- Print family (
Print,Println,Printf) writes directly to standard output (os.Stdout). - Sprint family (
Sprint,Sprintln,Sprintf) builds and returns astringinstead of writing anywhere. - Fprint family (
Fprint,Fprintln,Fprintf) writes to any value that satisfiesio.Writer— a file,os.Stderr, abytes.Buffer, an HTTP response, a network connection, anything.
Within each family, the *f variant (Printf, Sprintf, Fprintf) takes an explicit format string containing verbs like %d or %s that describe how to render each argument. The plain variant (Print, Sprint, Fprint) uses each argument’s default representation and concatenates them, adding a space between two operands only when neither one is a string. The *ln variant (Println, Sprintln, Fprintln) always separates every operand with a space and appends a trailing newline, regardless of type.
Under the hood, every one of these functions accepts its operands as ...any (a variadic slice of the empty interface). That is the key design fact about fmt: because arguments are untyped at compile time, the package cannot check at compile time whether you passed the right type or the right number of arguments for your format string — that all happens at runtime, using the reflect package to inspect each argument’s dynamic type and choose how to render it. When something doesn’t line up (a %d paired with a string, or a verb with no matching argument), fmt does not panic. Instead it writes a visible error marker straight into the output, like %!d(string=hello), so the mistake is impossible to miss. The go vet tool can catch many of these mismatches statically before you even run the program, which is why running go vet alongside go build is standard practice.
For any type, %v produces a default, “just show me the value” representation: numbers print as numbers, strings print unquoted, structs print as {field1 field2}, slices as [a b c], maps as map[key:value], and pointers as a hex address unless you use %v on the pointed-to struct via automatic dereferencing in some contexts. If a type has a method String() string, it satisfies the built-in fmt.Stringer interface — and because Go interfaces are satisfied implicitly (no implements keyword needed, just the right method set), any type you define automatically plugs into fmt‘s formatting simply by having that method. Whenever %v or %s encounters a value implementing Stringer, it calls String() and uses that instead of the default layout. The same happens for the built-in error interface: a value with an Error() string method is rendered by calling Error(). This is also how fmt.Errorf works — it builds an error value using the same format verbs, and the special %w verb wraps another error so it can later be unwrapped with errors.Is or errors.As.
fmt also handles the reverse direction: Sscanf, Fscanf, and Scanln parse formatted text back into typed variables using pointers (&x) as destinations, mirroring C’s scanf family. This lesson focuses on the output side, since that’s what you’ll reach for constantly, but it’s worth knowing the scanning functions exist for reading structured text and simple command-line input.
Syntax
A Printf-style format string is ordinary text interspersed with verbs. Each verb has the general shape:
%[flags][width][.precision]verb
- verb — a letter selecting how to render the next argument, e.g.
d(decimal integer),s(string),f(float),t(bool),v(default format). - flags — optional modifiers such as
-(left-justify),+(always show sign), or0(zero-pad). - width — minimum field width, e.g.
%6dpads a number to at least 6 characters. - precision — digits after the decimal point for floats (
%.2f), or max length for strings.
The most common verbs:
| Verb | Meaning |
|---|---|
%v |
Default format for the value’s type |
%+v |
Default format, plus struct field names |
%#v |
Go-syntax representation of the value |
%T |
The Go type of the value |
%d |
Base-10 integer |
%x / %X |
Hexadecimal integer (lower/upper case) |
%b |
Binary integer |
%f |
Decimal-point float, e.g. 3.140000 |
%.2f |
Float with 2 digits after the point |
%s |
String (or calls String() / Error() if defined) |
%q |
Double-quoted, escaped string |
%t |
Boolean, true or false |
%p |
Pointer address in hex |
%% |
A literal percent sign |
Examples
Example 1: The three printing styles
package main
import "fmt"
func main() {
name := "Gopher"
age := 15
fmt.Print("Hello, ", name, "!\n")
fmt.Println("Age:", age)
fmt.Printf("%s is %d years old.\n", name, age)
}
Output:
Hello, Gopher!
Age: 15
Gopher is 15 years old.
Print concatenates its operands with no automatic spaces here because every operand is a string, so the spacing has to be written into the strings themselves (note the trailing space after “Hello,”). Println always inserts a space between operands and a trailing newline, no matter the types involved. Printf gives full control by matching each verb in the format string to the next argument in order.
Example 2: Sprintf and format verbs
package main
import "fmt"
func main() {
pi := 3.14159265
count := 42
isReady := true
msg := fmt.Sprintf("pi=%.2f count=%d ready=%t hex=%x", pi, count, isReady, count)
fmt.Println(msg)
}
Output:
pi=3.14 count=42 ready=true hex=2a
Sprintf works exactly like Printf but returns the built string instead of writing it anywhere, which makes it useful for building log lines, error messages, or any string you need to hand off to something else. Notice %.2f rounds the float to two decimal places, and %x renders 42 as its hexadecimal form 2a.
Example 3: Making a type print itself with Stringer
package main
import "fmt"
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func main() {
p := Point{X: 3, Y: 4}
fmt.Println(p)
fmt.Printf("Point: %v\n", p)
fmt.Printf("Point: %s\n", p)
}
Output:
(3, 4)
Point: (3, 4)
Point: (3, 4)
Point satisfies fmt.Stringer purely by having a method named String() string — there is no implements Stringer declaration anywhere, because Go interfaces are satisfied implicitly by whatever type happens to have the right methods. Once that method exists, Println, and the %v/%s verbs in Printf, all call it automatically instead of falling back to the default {3 4} struct layout.
Example 4: Writing to different destinations with Fprintln and Errorf
package main
import (
"fmt"
"os"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("divide: cannot divide %d by 0", a)
}
return a / b, nil
}
func main() {
if _, err := divide(10, 0); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
}
result, err := divide(10, 2)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return
}
fmt.Println("result:", result)
}
Output:
error: divide: cannot divide 10 by 0
result: 5
fmt.Errorf builds an error using the same verb syntax as Printf. Fprintln writes to whatever io.Writer you give it — here os.Stderr for the error line, keeping it separate from normal program output on os.Stdout, which is exactly why command-line tools route errors to stderr and results to stdout.
How It Works Step by Step
Take a call like fmt.Printf("%s is %d\n", name, age) and trace what actually happens:
- 1. Because
Printf‘s signature isPrintf(format string, a ...any), the compiler packsnameandageinto a slice of type[]anyat the call site — no runtime type checking has happened yet. - 2. At runtime,
Printfscans the format string character by character, copying plain text straight to the output buffer. - 3. When it hits a
%, it parses the flags, width, precision, and verb letter that follow, then pulls the next value off the argument slice. - 4. Using the
reflectpackage, it inspects that value’s dynamic type. If the type implementsStringer,error, or the lower-levelfmt.Formatterinterface, that custom logic is invoked; otherwise a built-in default formatter for the value’s kind (int, string, struct, slice, etc.) runs. - 5. The formatted text is appended to an internal buffer as it’s produced.
- 6. Once the whole format string has been consumed, the buffer’s bytes are written out in one call to the underlying
io.Writer(os.StdoutforPrintf, a growing byte buffer forSprintf), and the function returns the number of bytes written and any write error.
Common Mistakes
Mistake 1: Using the wrong verb for a value’s type
package main
import "fmt"
func main() {
name := "Alice"
fmt.Printf("Score: %d\n", name)
}
Output:
Score: %!d(string=Alice)
%d expects an integer, but name is a string. This compiles fine — the compiler can’t know what type a verb expects — but at runtime fmt writes the mismatch directly into the output instead of the number you wanted. Use the verb that matches the argument’s actual type:
package main
import "fmt"
func main() {
name := "Alice"
score := 95
fmt.Printf("%s scored %d\n", name, score)
}
Output:
Alice scored 95
Mistake 2: Supplying too few arguments for the verbs
package main
import "fmt"
func main() {
fmt.Printf("%s scored %d\n", "Alice")
}
Output:
Alice scored %!d(MISSING)
Every verb needs a corresponding argument, in order. Here %d has nothing left to consume, so fmt reports %!d(MISSING) instead of crashing. Running go vet catches this particular mistake before you ever run the program — but the fix is simply to supply every argument the format string expects:
package main
import "fmt"
func main() {
fmt.Printf("%s scored %d\n", "Alice", 95)
}
Output:
Alice scored 95
Mistake 3: Assuming Print spaces things out like Println
package main
import "fmt"
func main() {
fmt.Print("Score", ":", 95, "\n")
}
Output:
Score:95
It’s easy to expect "Score : 95" here, but Print only inserts a space between two operands when neither one is a string — and every operand in this call is a string except 95, so no automatic spaces are added anywhere. If you want guaranteed spacing between every operand, use Println (which always spaces and adds a trailing newline) or write the spaces into a Printf format string yourself:
package main
import "fmt"
func main() {
fmt.Println("Score", ":", 95)
}
Output:
Score : 95
Best Practices
- Use
Printf/Sprintfwith explicit verbs instead ofPrintwhenever spacing matters — it’s far more predictable than relying onPrint‘s “space only between two non-strings” rule. - Run
go vetregularly (most editors do this automatically); it statically catches verb/argument mismatches thatgo buildalone will not. - Implement
String() stringon types you’ll frequently print or log (structs, custom enums, IDs) so%v/%sandPrintlnproduce readable output everywhere for free. - Prefer
%+vover%vwhen debugging structs — it includes field names and is much easier to read at a glance. - Send errors and diagnostics to
os.StderrviaFprintln/Fprintf, and reserveos.Stdoutfor a program’s actual output, so users and scripts can separate the two. - Use
fmt.Errorfwith the%wverb to wrap underlying errors instead of%v, so callers can inspect the chain witherrors.Is/errors.As. - Always check the error returned by
Fprintf/Fprintlnwhen writing to something that can fail, like a file or network connection — writes toos.Stdoutessentially never fail, but writes to otherio.Writers can.
Practice Exercises
- Write a program that declares a
float64price and anintquantity, then usesPrintfto print a line like3 items at $12.50 eachusing%dand%.2f. - Define a
Temperaturetype based onfloat64with aString()method that formats it as"23.5°C", then print a slice of threeTemperaturevalues withPrintlnand observe how each one renders. - Write a function that deliberately mismatches a verb and its argument (like
%dwith a string), print the result, and explain in a comment what the%!d(...)marker means and how you’d catch it before shipping.
Summary
fmthas three output families:Print*(to stdout),Sprint*(to a string), andFprint*(to anyio.Writer).Printadds spaces only between two non-string operands;Printlnalways adds spaces and a trailing newline;Printfgives full control via format verbs.- Verbs like
%v,%d,%s,%f,%t, and%Tselect how each argument is rendered; width, precision, and flags fine-tune the output. - A type satisfies
fmt.Stringerimplicitly just by having aString() stringmethod — no explicit declaration needed — and%v/%s/Printlnwill call it automatically. - Verb and argument mismatches are runtime issues, not compile errors, and show up as visible
%!verb(...)markers in the output rather than panics. fmt.Errorfbuilds errors with the same verb syntax, and%wwraps an inner error for later inspection witherrors.Is/errors.As.
