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 a string instead of writing anywhere.
  • Fprint family (Fprint, Fprintln, Fprintf) writes to any value that satisfies io.Writer — a file, os.Stderr, a bytes.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), or 0 (zero-pad).
  • width — minimum field width, e.g. %6d pads 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 is Printf(format string, a ...any), the compiler packs name and age into a slice of type []any at the call site — no runtime type checking has happened yet.
  • 2. At runtime, Printf scans 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 reflect package, it inspects that value’s dynamic type. If the type implements Stringer, error, or the lower-level fmt.Formatter interface, 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.Stdout for Printf, a growing byte buffer for Sprintf), 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/Sprintf with explicit verbs instead of Print whenever spacing matters — it’s far more predictable than relying on Print‘s “space only between two non-strings” rule.
  • Run go vet regularly (most editors do this automatically); it statically catches verb/argument mismatches that go build alone will not.
  • Implement String() string on types you’ll frequently print or log (structs, custom enums, IDs) so %v/%s and Println produce readable output everywhere for free.
  • Prefer %+v over %v when debugging structs — it includes field names and is much easier to read at a glance.
  • Send errors and diagnostics to os.Stderr via Fprintln/Fprintf, and reserve os.Stdout for a program’s actual output, so users and scripts can separate the two.
  • Use fmt.Errorf with the %w verb to wrap underlying errors instead of %v, so callers can inspect the chain with errors.Is/errors.As.
  • Always check the error returned by Fprintf/Fprintln when writing to something that can fail, like a file or network connection — writes to os.Stdout essentially never fail, but writes to other io.Writers can.

Practice Exercises

  • Write a program that declares a float64 price and an int quantity, then uses Printf to print a line like 3 items at $12.50 each using %d and %.2f.
  • Define a Temperature type based on float64 with a String() method that formats it as "23.5°C", then print a slice of three Temperature values with Println and observe how each one renders.
  • Write a function that deliberately mismatches a verb and its argument (like %d with a string), print the result, and explain in a comment what the %!d(...) marker means and how you’d catch it before shipping.

Summary

  • fmt has three output families: Print* (to stdout), Sprint* (to a string), and Fprint* (to any io.Writer).
  • Print adds spaces only between two non-string operands; Println always adds spaces and a trailing newline; Printf gives full control via format verbs.
  • Verbs like %v, %d, %s, %f, %t, and %T select how each argument is rendered; width, precision, and flags fine-tune the output.
  • A type satisfies fmt.Stringer implicitly just by having a String() string method — no explicit declaration needed — and %v/%s/Println will 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.Errorf builds errors with the same verb syntax, and %w wraps an inner error for later inspection with errors.Is/errors.As.