Go October 5, 2024 Aditya Rawas

Demystifying Go's fmt.Sprintf: A Practical Guide

Go’s fmt.Sprintf() is rooted in C’s printf family of functions. While C developers may find it familiar, developers coming from JavaScript, Python, or Ruby sometimes find the verb-based syntax confusing. This guide breaks down everything you need — from basic verbs to width formatting, binary output, writing to HTTP responses, and parsing strings.


Printf-Style Functions in Go

fmt.Sprintf() is one of many Printf-style functions. Knowing which to reach for saves you unnecessary string building:

FunctionReturnsWrites to
fmt.Sprintf(format, args...)string
fmt.Printf(format, args...)stdout
fmt.Fprintf(w, format, args...)any io.Writer
fmt.Errorf(format, args...)error
log.Printf(format, args...)log output

Most principles in this guide apply to all of them — they share the same format verb syntax.


Why Use fmt.Sprintf()?

Even if the syntax looks unfamiliar, fmt.Sprintf() separates constant string structure from variable content, making code far more readable:

Without:

result := "User " + username + " logged in at " + time + " from " + ip

With:

result := fmt.Sprintf("User %s logged in at %s from %s", username, time, ip)

Clean, predictable, and easy to extend.


The Format Verbs

General Verbs

VerbDescriptionExample
%vDefault format for any typefmt.Sprintf("%v", 42)"42"
%+vStruct with field namesfmt.Sprintf("%+v", p){Name:Alice Age:30}
%#vGo syntax representationfmt.Sprintf("%#v", p)main.Person{Name:"Alice", Age:30}
%TType of the valuefmt.Sprintf("%T", 3.14)"float64"

String and Byte Verbs

name := "Gopher"
fmt.Sprintf("%s", name)  // Gopher       — plain string
fmt.Sprintf("%q", name)  // "Gopher"     — double-quoted, escaped
fmt.Sprintf("%x", name)  // 476f70686572 — hex encoding of bytes

%q is especially useful for logging — it clearly shows whether a value is empty string or contains invisible characters.

Integer Verbs

n := 255
fmt.Sprintf("%d", n)   // 255        — decimal
fmt.Sprintf("%b", n)   // 11111111   — binary
fmt.Sprintf("%o", n)   // 377        — octal
fmt.Sprintf("%x", n)   // ff         — lowercase hex
fmt.Sprintf("%X", n)   // FF         — uppercase hex
fmt.Sprintf("%c", 65)  // A          — Unicode character

Binary (%b) and hex (%x) are useful for bitwise operations, color codes, and debugging memory addresses.

Float Verbs

f := 3.141592653589793
fmt.Sprintf("%f", f)    // 3.141593   — default precision (6 decimal places)
fmt.Sprintf("%.2f", f)  // 3.14       — 2 decimal places
fmt.Sprintf("%e", f)    // 3.141593e+00 — scientific notation
fmt.Sprintf("%g", f)    // 3.141592653589793 — shortest representation

Boolean Verb

fmt.Sprintf("%t", true)  // true
fmt.Sprintf("%t", false) // false

Width and Padding

Control field width and alignment with a number between % and the verb:

Right-Aligned (default)

fmt.Sprintf("%10s", "Go")    // "        Go"  — right-aligned in 10 chars
fmt.Sprintf("%10d", 42)      // "        42"  — right-aligned

Left-Aligned (with - flag)

fmt.Sprintf("%-10s|", "Go")  // "Go        |" — left-aligned
fmt.Sprintf("%-10d|", 42)    // "42        |" — left-aligned

Zero-Padding Numbers

fmt.Sprintf("%05d", 42)      // "00042" — zero-padded to 5 digits
fmt.Sprintf("%08.2f", 3.14)  // "00003.14" — zero-padded float

Zero-padding is commonly used for:

  • Log line numbers: %06d
  • File names in sequences: frame_0001.png
  • Fixed-width ID formatting

Combining Width and Precision

fmt.Sprintf("%10.2f", 3.14159) // "      3.14" — width 10, 2 decimal places

Building a Formatted Table

rows := []struct{ Name string; Score int }{
    {"Alice", 1250},
    {"Bob", 875},
    {"Charlie", 2100},
}

fmt.Println(fmt.Sprintf("%-10s %6s", "NAME", "SCORE"))
fmt.Println(fmt.Sprintf("%-10s %6s", "----", "-----"))
for _, r := range rows {
    fmt.Println(fmt.Sprintf("%-10s %6d", r.Name, r.Score))
}

Output:

NAME        SCORE
----        -----
Alice        1250
Bob           875
Charlie      2100

Argument Indexing

Reference arguments by position using [n] before the verb:

red, blue, orange := "Red", "Blue", "Orange"
fmt.Sprintf("%[1]s %[3]s %[2]s", red, orange, blue)
// Output: Red Blue Orange

This is useful when the same value appears multiple times:

lang := "Go"
fmt.Sprintf("%[1]s is great. I love %[1]s.", lang)
// Output: Go is great. I love Go.

Writing to HTTP Responses with fmt.Fprintf

fmt.Fprintf writes directly to any io.Writer — including http.ResponseWriter:

func handler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    fmt.Fprintf(w, "Hello, %s! The time is %s.", name, time.Now().Format(time.Kitchen))
}

This is more efficient than building a string with Sprintf and then writing it — it writes directly to the output stream without an intermediate allocation.

Writing to Files

file, err := os.Create("output.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

fmt.Fprintf(file, "Generated at: %s\n", time.Now().Format(time.RFC3339))
fmt.Fprintf(file, "Records processed: %d\n", count)

Structured Error Messages with fmt.Errorf

func getUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err)
    }
    return user, nil
}

The %w verb wraps the error — callers can unwrap it with errors.Is or errors.As:

if errors.Is(err, sql.ErrNoRows) {
    // handle not found
}

Use %w when wrapping errors you expect callers to inspect. Use %v when you want to include the error message as context only.


Parsing Strings with fmt.Sscanf

The reverse of Sprintffmt.Sscanf parses a formatted string back into variables:

var name string
var age int

n, err := fmt.Sscanf("Alice 30", "%s %d", &name, &age)
fmt.Println(name, age, n) // Alice 30 2

Sscanf returns the number of successfully scanned items and an error if scanning fails. Useful for parsing simple fixed-format input without regex.


Common Errors and Fixes

Wrong type for the verb

fmt.Sprintf("%d", "hello")  // %!d(string=hello) — Go shows the actual value
fmt.Sprintf("%s", "hello")  // hello ✓

Too many arguments

fmt.Sprintf("%d %d", 3, 2, 1)  // %!(EXTRA int=1) — Go appends the extra
fmt.Sprintf("%d %d %d", 3, 2, 1) // ✓

Non-integer for width/precision

fmt.Sprintf("%*s", 10.5, "hi")   // runtime error
fmt.Sprintf("%*s", 10, "hi")     // "        hi" ✓

Invalid argument index

fmt.Sprintf("%[2]s", "only-one")  // %!s(BADINDEX)
fmt.Sprintf("%[1]s", "only-one")  // "only-one" ✓

Key Takeaways

  • %v formats any type with its default representation — great for quick debugging.
  • %s for strings, %d for integers, %f for floats, %t for booleans, %q for quoted/escaped strings.
  • %b, %o, %x, %X format integers in binary, octal, and hex.
  • Use width (%10s) for right-aligned output and - flag (%-10s) for left-aligned.
  • Use %05d for zero-padded numbers in sequences and IDs.
  • Use argument indexing (%[1]s) to reuse or reorder arguments.
  • Use fmt.Fprintf to write directly to io.Writer targets (HTTP responses, files) — no intermediate string needed.
  • Use %w in fmt.Errorf to wrap errors that callers need to inspect with errors.Is/errors.As.
Aditya Rawas

Written by

Aditya Rawas

Full-stack engineer writing deep-dives on JavaScript, TypeScript, React, AWS, Docker, and Kubernetes. Passionate about making complex engineering concepts accessible to developers at every level.