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:
| Function | Returns | Writes 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
| Verb | Description | Example |
|---|---|---|
%v | Default format for any type | fmt.Sprintf("%v", 42) → "42" |
%+v | Struct with field names | fmt.Sprintf("%+v", p) → {Name:Alice Age:30} |
%#v | Go syntax representation | fmt.Sprintf("%#v", p) → main.Person{Name:"Alice", Age:30} |
%T | Type of the value | fmt.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 Sprintf — fmt.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
%vformats any type with its default representation — great for quick debugging.%sfor strings,%dfor integers,%ffor floats,%tfor booleans,%qfor quoted/escaped strings.%b,%o,%x,%Xformat integers in binary, octal, and hex.- Use width (
%10s) for right-aligned output and-flag (%-10s) for left-aligned. - Use
%05dfor zero-padded numbers in sequences and IDs. - Use argument indexing (
%[1]s) to reuse or reorder arguments. - Use
fmt.Fprintfto write directly toio.Writertargets (HTTP responses, files) — no intermediate string needed. - Use
%winfmt.Errorfto wrap errors that callers need to inspect witherrors.Is/errors.As.
Related Articles
Understanding Structs in Go: The Foundation of Data Modeling
Learn how Go structs work — defining custom types, creating instances, passing by value vs pointer, adding methods, constructor functions, struct tags for JSON, anonymous structs, and struct embedding.
GoCreating Your First Go Module: A Step-by-Step Tutorial
Learn how to create, link, and publish Go modules — build a reusable math utilities module, use go mod init, the replace directive, go workspaces, semantic versioning, and publish to pkg.go.dev.
GoGo Data Types: A Comprehensive Guide for Developers
Explore every Go data type — bool, string, int, float, byte, rune, arrays, slices, maps, pointers, and interfaces — with practical examples, type conversion, and best practices for Go beginners.
Written by
Aditya RawasFull-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.