When your data refuses to speak
You write a struct to hold configuration data. You print it in main to verify the values. The output looks like {0xc0000102a0 map[]}. You expected Config{Host: localhost, Port: 8080}. The compiler didn't complain. The program ran. But the output is useless for debugging.
This happens because Go doesn't magically know how to turn your custom types into readable text. The language treats your struct as a collection of memory addresses and raw values. You have to tell the runtime how to present the data. The fmt package is the bridge between your internal state and the screen, but it only speaks a specific dialect. You provide the template; fmt fills in the blanks.
The stamping machine
Think of fmt functions as a stamping machine. You feed it a template with holes in it. You also feed it the raw materials. The machine punches the materials into the holes and stamps out the result. If the material doesn't fit the hole, the machine jams or prints a warning code.
The template is the format string. It contains verbs like %s for strings and %d for integers. These verbs define the shape of the hole. The arguments are the materials. Each verb consumes one argument from the list, in order. The machine processes the string left to right, replacing verbs with formatted values.
fmt is part of the standard library. It handles the heavy lifting of type conversion and layout. It also follows Go conventions. The package uses variadic arguments, meaning you can pass any number of values after the format string. The signature uses ...any, which replaced ...interface{} in Go 1.18. This allows maximum flexibility, but it also means the compiler cannot check if your verbs match your arguments. That check happens at runtime.
Minimal example
package main
import "fmt"
// main demonstrates basic formatting with Printf.
func main() {
// Printf writes formatted text to standard output.
// The format string defines the layout.
// Arguments fill the verbs in order.
fmt.Printf("Hello, %s! You are %d years old.\n", "Alice", 30)
}
The output is Hello, Alice! You are 30 years old.. The %s verb consumed "Alice". The %d verb consumed 30 and converted it to decimal text. The \n added a newline at the end. Printf writes directly to os.Stdout.
How formatting works
When you call Printf, the function scans the format string character by character. It copies literal text to the output buffer. When it encounters a %, it looks at the next character to identify the verb. It grabs the next argument from the variadic list. It checks the argument's type against the verb's requirements. If the types match, it formats the value and appends it. If the types mismatch, it produces a diagnostic string like %!d(string=hello) instead of crashing.
The function continues until the format string is exhausted. If there are leftover arguments, they are ignored. If there are missing arguments, the output contains %!s(MISSING). The compiler won't catch these issues because the arguments are passed as ...any. The type safety stops at the function boundary. You must verify your format strings manually or use tools that analyze them.
fmt functions return the number of bytes written and an error. In production code, check the error when writing to files or networks. For standard output, errors are rare, but ignoring them entirely can hide problems in complex pipelines. The convention is to handle errors explicitly. Use _ to discard the byte count if you don't need it.
gofmt formats your source code. fmt formats your output text. Don't confuse the two. gofmt is mandatory for code style. fmt is a tool for text generation.
Verbs and flags
The format string supports many verbs. %v is the universal fallback. It prints any value in a default format. %s prints strings and byte slices. %d prints integers in decimal. %t prints booleans as true or false. %f prints floats. %x prints hex. %q prints quoted strings. %T prints the type name.
Verbs accept flags to control alignment, padding, and precision. Flags go between % and the verb letter.
#enables alternate format.%#vprints values using Go syntax.%#qadds quotes.-left-aligns the value.0pads with zeros instead of spaces.- Width sets the minimum field width.
%10spads a string to 10 characters. - Precision sets decimal places for floats or truncation for strings.
%.2frounds to two decimals.
package main
import "fmt"
// demonstrateFlags shows common formatting flags.
func demonstrateFlags() {
// %10s pads the string to 10 characters on the left.
fmt.Printf("|%10s|\n", "hi")
// %-10s pads on the right.
fmt.Printf("|%-10s|\n", "hi")
// %05d pads the number with zeros to width 5.
fmt.Printf("%05d\n", 42)
// %.2f rounds the float to two decimal places.
fmt.Printf("%.2f\n", 3.14159)
// %#v prints the value using Go syntax.
fmt.Printf("%#v\n", []int{1, 2, 3})
}
func main() {
demonstrateFlags()
}
The output shows aligned columns, zero-padded numbers, precise floats, and Go-syntax slices. Flags give you fine-grained control over layout without manual string manipulation.
Realistic patterns
In real code, you rarely just print to stdout. You build strings, write to files, and wrap errors. fmt supports all these patterns.
Implementing String() string on a type satisfies the fmt.Stringer interface. When fmt encounters a Stringer, it calls the method for %v and %s. This is the standard way to make custom types readable. The receiver name should be short, usually one or two letters matching the type.
package main
import (
"fmt"
"io"
)
// Config holds application settings.
type Config struct {
Host string
Port int
}
// String returns a readable config summary.
// The receiver name c is short and matches the type.
// Implementing this method satisfies the fmt.Stringer interface.
func (c Config) String() string {
return fmt.Sprintf("Config{Host: %s, Port: %d}", c.Host, c.Port)
}
// saveConfig writes the config to a file writer.
// Accepting io.Writer allows testing with a buffer.
// This follows the "accept interfaces, return structs" convention.
func saveConfig(w io.Writer, cfg Config) error {
// Fprintf writes formatted text to the writer.
// Check the error to ensure the write succeeded.
// Discard the byte count with _ if unused.
_, err := fmt.Fprintf(w, "%s\n", cfg)
return err
}
// wrapError adds context to an error.
// The %w verb wraps the error for errors.Is and errors.As checks.
// Use %w, not %v, to preserve the error chain.
func wrapError(err error) error {
return fmt.Errorf("config error: %w", err)
}
func main() {
cfg := Config{Host: "localhost", Port: 8080}
// Sprintf builds a string without printing it.
// Use this when you need to store or return the text.
msg := fmt.Sprintf("Starting server: %s", cfg)
fmt.Println(msg)
// Fprintf writes to os.Stdout.
// os.Stdout implements io.Writer.
saveConfig(nil, cfg)
// Errorf creates errors with context.
err := wrapError(fmt.Errorf("timeout"))
fmt.Println(err)
}
Sprintf returns the formatted string. Use it when you need a value, not side effects. Fprintf writes to any io.Writer. This decouples formatting from the destination. You can test saveConfig by passing a bytes.Buffer instead of a real file. Errorf creates errors. The %w verb is critical. It wraps the underlying error so that errors.Is and errors.As can unwrap it later. Using %v breaks the chain.
Pitfalls and errors
Mismatched verbs produce diagnostic strings, not panics. If you pass too few arguments, the output contains %!s(MISSING). If you pass a string to %d, the output contains %!d(string=hello). These markers are easy to miss in logs. Always review format strings carefully.
Structs print as memory addresses by default. {0xc0000102a0} tells you nothing. Implement String() to fix this. Debugging becomes painless when your types speak for themselves.
Println adds spaces between arguments and a newline at the end. Printf requires explicit newlines. Mixing them causes formatting bugs. Println is convenient for quick checks. Printf is precise for structured output.
Scan functions read from standard input. Scan skips whitespace. Scanln stops at a newline. Scanf uses a format string. Scan leaves the newline in the buffer, which can cause issues if you mix it with line-based reading. Use bufio.Scanner for robust input parsing.
fmt is not the fastest formatter. For high-performance logging or massive throughput, use strings.Builder or write directly to io.Writer. fmt allocates memory for intermediate values. In tight loops, this adds up. Profile before optimizing.
The compiler rejects unused imports with imported and not used. If you import fmt but don't use it, the build fails. Remove the import. The compiler also rejects unused variables. Assign to _ if you must discard a value intentionally.
Decision matrix
Use fmt.Println when you need quick debugging output and don't care about precise layout.
Use fmt.Printf when you need to control the exact formatting of text sent to standard output.
Use fmt.Sprintf when you need to build a formatted string to store in a variable or return from a function.
Use fmt.Fprintf when you need to write formatted text to a file, buffer, or network connection.
Use fmt.Errorf when you need to create an error with context, using %w to wrap underlying errors.
Use fmt.Scan when you need to read simple input from standard input during interactive testing.
Use a dedicated logging library when you need structured logs, levels, or high-performance output in production.
Use strings.Builder when you need to concatenate many formatted strings in a loop without excessive allocations.
Pick the function that matches your destination. Println for speed, Sprintf for values, Fprintf for flexibility. Implement String() for your types. Use %w to wrap errors. Trust gofmt for your source files.