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
Platform-Independent SIMD in Go: A Practical Guide to the New simd Package
Explore Go's platform-independent SIMD package: how it works, performance gains, portable vector code, and practical examples for high-performance Go apps.
GoWhat Zig Feels Like Coming from Go: A Systems Programming Comparison
A practical comparison of Zig and Go for backend engineers — memory management, error handling, concurrency, and when to reach for each language in production.
GoUnderstanding 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.
Never Miss an Article
Stay Updated
Get new deep-dives on JavaScript, TypeScript, Go, and cloud-native engineering delivered to your reader.
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.