Fmt #
The fmt package is one of the most imported packages in Go code — there’s almost no Go program that doesn’t touch it. It handles two fundamental needs: printing output to the console or any stream, and formatting values into strings with full control over their appearance. What makes fmt interesting isn’t just its Println capability, but the verb system that lets you control the representation of every value precisely — from integers in hexadecimal format, to structs with their field names, to any value with a valid Go syntax representation. This article covers the entire fmt package: how it works, all the important verbs, the differences between its functions, and how to use it effectively in production code.
Three Function Families #
The fmt package organizes its functions into three families by purpose: print (print to stdout), fprint (print to any io.Writer), and sprint (return as a string). All three have consistent naming patterns.
| Family | Functions | Purpose |
|---|---|---|
Print, Println, Printf | Print to os.Stdout | |
| Fprint | Fprint, Fprintln, Fprintf | Print to an io.Writer (file, buffer, stderr, etc.) |
| Sprint | Sprint, Sprintln, Sprintf | Return as a string |
| Scan | Scan, Scanln, Scanf | Read input from os.Stdin |
| Fscan | Fscan, Fscanln, Fscanf | Read input from an io.Reader |
| Sscan | Sscan, Sscanln, Sscanf | Read input from a string |
| Errorf | Errorf | Create an error with a formatted string |
The naming pattern is easy to understand: the f suffix means it accepts a format string, the ln suffix means it adds a newline and spaces between arguments automatically. Without a suffix, spaces are added only if both operands aren’t strings.
flowchart LR
Input["Go values\n(int, string, struct, ...)"]
subgraph fmt["package fmt"]
direction TB
P["Print / Println / Printf"]
FP["Fprint / Fprintln / Fprintf"]
SP["Sprint / Sprintln / Sprintf"]
SC["Scan / Scanln / Scanf"]
ERR["Errorf"]
end
Stdout["os.Stdout"]
Writer["io.Writer\n(file, buffer, HTTP, stderr)"]
Str["string"]
StdinSrc["os.Stdin / io.Reader / string"]
ErrOut["error"]
Input --> P --> Stdout
Input --> FP --> Writer
Input --> SP --> Str
StdinSrc --> SC --> Input
Input --> ERR --> ErrOut
style fmt fill:#f0f4ff,stroke:#4f86c6
style Stdout fill:#e8f5e9,stroke:#4caf50
style Writer fill:#e8f5e9,stroke:#4caf50
style Str fill:#e8f5e9,stroke:#4caf50
style ErrOut fill:#fce4ec,stroke:#e91e63package main
import (
"fmt"
"os"
)
func main() {
name := "Gopher"
age := 15
// Print — no automatic newline
fmt.Print("Hello, ")
fmt.Print(name)
fmt.Print("\n")
// Println — adds a newline, spaces between arguments
fmt.Println("Hello,", name, "— age:", age)
// Printf — format with verbs
fmt.Printf("Name: %s, Age: %d years old\n", name, age)
// Sprintf — return a string (doesn't print)
message := fmt.Sprintf("Welcome, %s!", name)
fmt.Println(message)
// Fprintf — print to an io.Writer (stderr in this example)
fmt.Fprintf(os.Stderr, "Error: user %s not found\n", name)
}
Format Verbs — The Core of fmt #
Verbs are format codes starting with % followed by one or more characters that determine how a value is represented. Understanding verbs is the key to using fmt effectively.
flowchart TD
V["fmt verbs"] --> General["General — all types"]
V --> Int["Integer"]
V --> Flt["Float"]
V --> Str["String & Bytes"]
V --> Misc["Others"]
General --> vv["%v — default representation"]
General --> vpv["%+v — struct with field names"]
General --> vhv["%#v — full Go syntax"]
General --> vT["%T — the type name"]
Int --> vd["%d — decimal"]
Int --> vb["%b — binary"]
Int --> vx["%x / %X — hexadecimal"]
Int --> vo["%o — octal"]
Int --> vc["%c — Unicode character"]
Int --> vq["%q — quoted character"]
Flt --> vf["%f — fixed decimal"]
Flt --> ve["%e / %E — scientific notation"]
Flt --> vg["%g — shortest format"]
Str --> vs["%s — plain string"]
Str --> vsq["%q — with quotes & escaping"]
Str --> vsx["%x — hex of bytes"]
Misc --> vt["%t — boolean"]
Misc --> vp["%p — pointer / address"]
style V fill:#4f86c6,color:#fff
style General fill:#e3f2fd
style Int fill:#e8f5e9
style Flt fill:#fff3e0
style Str fill:#f3e5f5
style Misc fill:#fce4ecGeneral Verbs for All Types #
value := 42
pi := 3.14159
name := "Go"
active := true
// %v — the default representation, a safe choice for all types
fmt.Printf("%v\n", value) // 42
fmt.Printf("%v\n", pi) // 3.14159
fmt.Printf("%v\n", name) // Go
fmt.Printf("%v\n", active) // true
// %+v — for structs, adds field names
type User struct {
Name string
Email string
Age int
}
p := User{"Budi", "[email protected]", 30}
fmt.Printf("%v\n", p) // {Budi [email protected] 30}
fmt.Printf("%+v\n", p) // {Name:Budi Email:[email protected] Age:30}
// %#v — Go syntax representation (useful for debugging)
fmt.Printf("%#v\n", p) // main.User{Name:"Budi", Email:"[email protected]", Age:30}
fmt.Printf("%#v\n", []int{1, 2, 3}) // []int{1, 2, 3}
// %T — the type of the value
fmt.Printf("%T\n", value) // int
fmt.Printf("%T\n", pi) // float64
fmt.Printf("%T\n", p) // main.User
fmt.Printf("%T\n", &p) // *main.User
Integer Verbs #
n := 255
// Numeric representations
fmt.Printf("%d\n", n) // 255 — decimal (most common)
fmt.Printf("%b\n", n) // 11111111 — binary
fmt.Printf("%o\n", n) // 377 — octal
fmt.Printf("%x\n", n) // ff — lowercase hexadecimal
fmt.Printf("%X\n", n) // FF — uppercase hexadecimal
fmt.Printf("%#x\n", n) // 0xff — hex with the 0x prefix
fmt.Printf("%#o\n", n) // 0377 — octal with the 0 prefix
// Unicode characters
fmt.Printf("%c\n", 65) // A
fmt.Printf("%c\n", 9829) // ♥
fmt.Printf("%U\n", 65) // U+0041 — Unicode code point format
fmt.Printf("%q\n", 65) // 'A' — quoted character
// Width and padding
fmt.Printf("%5d\n", 42) // 42 — right-aligned, width 5
fmt.Printf("%-5d|\n", 42) // 42 | — left-aligned, width 5
fmt.Printf("%05d\n", 42) // 00042 — zero padding
fmt.Printf("%+d\n", 42) // +42 — always show the sign
fmt.Printf("%+d\n", -42) // -42
Float Verbs #
f := 3.14159265358979
// Basic formats
fmt.Printf("%f\n", f) // 3.141593 — default 6 decimals
fmt.Printf("%e\n", f) // 3.141593e+00 — lowercase scientific notation
fmt.Printf("%E\n", f) // 3.141593E+00 — uppercase scientific notation
fmt.Printf("%g\n", f) // 3.14159265358979 — shortest format
fmt.Printf("%G\n", f) // 3.14159265358979
// Precision control: %[width].[precision]f
fmt.Printf("%.2f\n", f) // 3.14 — 2 decimal places
fmt.Printf("%.5f\n", f) // 3.14159 — 5 decimal places
fmt.Printf("%8.2f\n", f) // 3.14 — width 8, 2 decimals
fmt.Printf("%08.2f\n", f) // 00003.14 — zero padding
fmt.Printf("%-8.2f|\n", f) // 3.14 | — left-aligned
// Special values
fmt.Printf("%f\n", math.Inf(1)) // +Inf
fmt.Printf("%f\n", math.Inf(-1)) // -Inf
fmt.Printf("%f\n", math.NaN()) // NaN
String and Byte Verbs #
s := "Hello, World!"
b := []byte{72, 101, 108, 108, 111}
// Strings
fmt.Printf("%s\n", s) // Hello, World! — plain string
fmt.Printf("%q\n", s) // "Hello, World!" — with quotes and escaping
fmt.Printf("%x\n", s) // 48616c6c6f2c20576f... — hex of the string's bytes
// Byte slices
fmt.Printf("%s\n", b) // Hello — interpreted as a string
fmt.Printf("%x\n", b) // 48656c6c6f
fmt.Printf("%X\n", b) // 48656C6C6F
fmt.Printf("% x\n", b) // 48 65 6c 6c 6f — space between bytes
// Width and alignment
fmt.Printf("%10s\n", "Go") // Go — right-aligned
fmt.Printf("%-10s|\n", "Go") // Go | — left-aligned
fmt.Printf("%.3s\n", "Golang") // Gol — truncate to 3 characters
Boolean and Pointer Verbs #
// Booleans
fmt.Printf("%t\n", true) // true
fmt.Printf("%t\n", false) // false
// Pointers — the memory address in hexadecimal
x := 42
fmt.Printf("%p\n", &x) // 0xc0000b4008 (address varies)
slice := []int{1, 2, 3}
fmt.Printf("%p\n", slice) // 0xc0000b4020 — pointer to the first element
Formatting Structs and Custom Types #
fmt supports two ways to customize the display of your types: implementing the fmt.Stringer interface for the default display, and the fmt.GoStringer interface for the Go syntax representation.
The Stringer Interface #
type Coordinate struct {
Lat float64
Lon float64
}
// ANTI-PATTERN: no Stringer — uninformative output
k := Coordinate{-6.2088, 106.8456}
fmt.Println(k) // {-6.2088 106.8456} — hard to read
// CORRECT: implement Stringer for a meaningful display
func (k Coordinate) String() string {
latDir := "N"
if k.Lat < 0 {
latDir = "S"
}
lonDir := "E"
if k.Lon < 0 {
lonDir = "W"
}
return fmt.Sprintf("%.4f°%s, %.4f°%s",
math.Abs(k.Lat), latDir,
math.Abs(k.Lon), lonDir)
}
fmt.Println(k) // 6.2088°S, 106.8456°E
fmt.Printf("%v\n", k) // 6.2088°S, 106.8456°E
fmt.Printf("%s\n", k) // 6.2088°S, 106.8456°E
The GoStringer Interface #
// GoStringer — for %#v
func (k Coordinate) GoString() string {
return fmt.Sprintf("Coordinate{Lat: %g, Lon: %g}", k.Lat, k.Lon)
}
fmt.Printf("%#v\n", k) // Coordinate{Lat: -6.2088, Lon: 106.8456}
The Formatter Interface for Full Control #
For very specific formatting needs, implement fmt.Formatter:
type Matrix struct {
data [][]float64
rows int
cols int
}
func (m Matrix) Format(f fmt.State, verb rune) {
switch verb {
case 'v', 's':
for i, row := range m.data {
if i > 0 {
fmt.Fprint(f, "\n")
}
fmt.Fprint(f, "[")
for j, val := range row {
if j > 0 {
fmt.Fprint(f, " ")
}
fmt.Fprintf(f, "%6.2f", val)
}
fmt.Fprint(f, "]")
}
case 'q':
// another special format
fmt.Fprintf(f, "Matrix(%dx%d)", m.rows, m.cols)
}
}
Errorf and Error Wrapping #
fmt.Errorf is the idiomatic way to create errors with additional context. Since Go 1.13, it supports error wrapping with the %w verb, which allows errors.Is and errors.As to work on wrapped errors.
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
// ANTI-PATTERN: create a new error that loses context
func findUser(id int) error {
return fmt.Errorf("user not found") // the id information is lost
}
// ALSO ANTI-PATTERN: use %v — can't be unwrapped
func findUser2(id int) error {
return fmt.Errorf("findUser %d: %v", id, ErrNotFound)
// errors.Is(err, ErrNotFound) will return false!
}
// CORRECT: use %w for wrapping — can be unwrapped
func findUser3(id int) error {
if id <= 0 {
return fmt.Errorf("findUser: invalid id %d", id)
}
if id > 1000 {
return fmt.Errorf("findUser %d: %w", id, ErrNotFound)
}
return nil
}
// Usage
err := findUser3(9999)
if err != nil {
fmt.Println(err) // findUser 9999: not found
// errors.Is works because %w is used
if errors.Is(err, ErrNotFound) {
fmt.Println("handle: data doesn't exist in the database")
}
}
Good error naming follows the convention: functionName: error detail. This convention makes the error stack easy to read when the error is wrapped across several layers:
sequenceDiagram
participant H as handler
participant S as service
participant R as repository
participant DB as database
H->>S: service(id)
S->>R: repository(id)
R->>DB: query(id)
DB-->>R: ErrNotFound
R-->>S: fmt.Errorf("repository %d: %w", id, err)
S-->>H: fmt.Errorf("service: %w", err)
H-->>H: fmt.Errorf("handler: %w", err)
Note over H: errors.Is(err, ErrNotFound) → true
Note over H: err.Error() → "handler: service: repository 42: not found"// The error formed when wrapped in layers
// "handler: service: repository: not found"
func repository(id int) error {
return fmt.Errorf("repository %d: %w", id, ErrNotFound)
}
func service(id int) error {
if err := repository(id); err != nil {
return fmt.Errorf("service: %w", err)
}
return nil
}
func handler(id int) error {
if err := service(id); err != nil {
return fmt.Errorf("handler: %w", err)
}
return nil
}
err := handler(42)
fmt.Println(err)
// handler: service: repository 42: not found
Fprintf — Writing to an io.Writer #
fmt.Fprintf is the most flexible version of Printf because it accepts any io.Writer as the output destination — a file, buffer, network connection, HTTP response, or a custom io.Writer implementation.
flowchart LR
FP["fmt.Fprintf\n(w io.Writer, format, args)"]
FP --> Stderr["os.Stderr\nlog errors"]
FP --> File["os.File\nwrite to a file"]
FP --> Buf["bytes.Buffer\nbuild a string incrementally"]
FP --> SB["strings.Builder\nstring efficiently"]
FP --> HTTP["http.ResponseWriter\nHTTP response"]
FP --> Net["net.Conn\nnetwork communication"]
FP --> Custom["custom io.Writer\nyour own implementation"]
style FP fill:#4f86c6,color:#fffimport (
"bytes"
"fmt"
"os"
"strings"
)
// Writing to os.Stderr — for error logs
fmt.Fprintf(os.Stderr, "[ERROR] %s: %v\n", "connection failed", err)
// Writing to a file
file, _ := os.Create("output.txt")
defer file.Close()
fmt.Fprintf(file, "Report dated %s\n", time.Now().Format("2006-01-02"))
// Writing to a bytes.Buffer — for building a string incrementally
var buf bytes.Buffer
for i := 1; i <= 5; i++ {
fmt.Fprintf(&buf, "item %d\n", i)
}
result := buf.String()
// Writing to a strings.Builder — more efficient than bytes.Buffer for strings
var sb strings.Builder
for i := 0; i < 3; i++ {
fmt.Fprintf(&sb, "line %d\n", i+1)
}
fmt.Print(sb.String())
Pattern: HTTP Response Writer #
http.ResponseWriter implements io.Writer, so fmt.Fprintf can be used directly to write HTTP responses:
import "net/http"
func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Hello, %s!\n", name)
fmt.Fprintf(w, "Server time: %s\n", time.Now().Format(time.RFC3339))
}
Sprintf — Building Formatted Strings #
fmt.Sprintf returns a formatted string without printing it. This is useful for building dynamic messages, log messages, queries, or values that will be processed further.
// Building dynamic messages
func welcomeMessage(name string, level int) string {
return fmt.Sprintf("Welcome back, %s! Your level: %d", name, level)
}
// Custom date formatting
func formatIndonesianDate(t time.Time) string {
months := []string{
"", "January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
}
return fmt.Sprintf("%d %s %d", t.Day(), months[t.Month()], t.Year())
}
// Building cache/map keys
func cacheKey(userID int, resource string) string {
return fmt.Sprintf("user:%d:%s", userID, resource)
}
// ANTI-PATTERN: string concatenation in a loop — repeated allocations
func makeCSVBad(data [][]string) string {
result := ""
for _, row := range data {
for j, cell := range row {
if j > 0 {
result += ","
}
result += cell
}
result += "\n"
}
return result
}
// CORRECT: use strings.Builder with WriteString for better performance
func makeCSVGood(data [][]string) string {
var sb strings.Builder
for _, row := range data {
for j, cell := range row {
if j > 0 {
sb.WriteByte(',')
}
sb.WriteString(cell)
}
sb.WriteByte('\n')
}
return sb.String()
}
Scan — Reading Input #
The Scan family reads values from input. It’s used less often than Print because modern Go applications usually read input from files, HTTP requests, or databases — not interactive stdin. But for CLI tools, it’s useful.
var name string
var age int
// Scanln — read one line, split by spaces
fmt.Print("Enter name and age: ")
n, err := fmt.Scanln(&name, &age)
fmt.Printf("Read %d values: name=%s, age=%d\n", n, name, age)
// Scanf — read with a specific format
var x, y float64
fmt.Print("Enter coordinates (x,y): ")
fmt.Scanf("%f,%f", &x, &y)
fmt.Printf("Coordinates: (%.2f, %.2f)\n", x, y)
// Sscan — read from a string (useful for parsing)
input := "Jakarta -6.2088 106.8456"
var city string
var lat, lon float64
fmt.Sscan(input, &city, &lat, &lon)
fmt.Printf("City: %s, Coordinates: %.4f, %.4f\n", city, lat, lon)
// Sscanf — read from a string with a format
date := "2024-03-15"
var year, month, day int
fmt.Sscanf(date, "%d-%d-%d", &year, &month, &day)
fmt.Printf("Year: %d, Month: %d, Day: %d\n", year, month, day)
fmt.Scanandfmt.Scanlnaren’t suitable for input containing spaces becauseScansplits values by whitespace. To read a full line including spaces, usebufio.Scannerorbufio.Reader.ReadString('\n').
The %w Verb and Multiple Wrapping (Go 1.20+) #
Since Go 1.20, fmt.Errorf supports multiple error wrapping — one error can wrap several errors at once using multiple %w in a single format string:
import "errors"
var (
ErrConnection = errors.New("connection failed")
ErrTimeout = errors.New("timeout")
)
// Go 1.20+: multiple error wrapping
func processData() error {
return fmt.Errorf("processData: %w and %w", ErrConnection, ErrTimeout)
}
err := processData()
fmt.Println(err) // processData: connection failed and timeout
// errors.Is works for both
fmt.Println(errors.Is(err, ErrConnection)) // true
fmt.Println(errors.Is(err, ErrTimeout)) // true
// To access all wrapped errors
var joinedErr interface{ Unwrap() []error }
if errors.As(err, &joinedErr) {
for _, e := range joinedErr.Unwrap() {
fmt.Printf(" - %v\n", e)
}
}
Production Application Patterns #
A Simple Logger with Fprintf #
import (
"fmt"
"os"
"time"
)
type Level int
const (
DEBUG Level = iota
INFO
WARN
ERROR
)
func (l Level) String() string {
switch l {
case DEBUG:
return "DEBUG"
case INFO:
return "INFO"
case WARN:
return "WARN"
case ERROR:
return "ERROR"
default:
return "UNKNOWN"
}
}
func log(level Level, format string, args ...any) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
writer := os.Stdout
if level >= ERROR {
writer = os.Stderr
}
fmt.Fprintf(writer, "[%s] %s — "+format+"\n",
append([]any{timestamp, level}, args...)...)
}
// Usage
log(INFO, "server running on port %d", 8080)
log(ERROR, "failed to connect to the database: %v", err)
String Templates for Notifications #
type Notification struct {
User string
Action string
Object string
Time time.Time
}
func (n Notification) ShortMessage() string {
return fmt.Sprintf("%s %s %s", n.User, n.Action, n.Object)
}
func (n Notification) FullMessage() string {
return fmt.Sprintf(
"User %q performed %s on %q at %s",
n.User,
n.Action,
n.Object,
n.Time.Format("02 Jan 2006 at 15:04"),
)
}
notif := Notification{
User: "Budi",
Action: "edited",
Object: "the proposal document",
Time: time.Now(),
}
fmt.Println(notif.ShortMessage())
// Budi edited the proposal document
fmt.Println(notif.FullMessage())
// User "Budi" performed edited on "the proposal document" at 15 Mar 2024 at 14:30
Debugging with %#v and %+v #
type Config struct {
Host string
Port int
Debug bool
MaxConn int
Timeout time.Duration
}
cfg := Config{
Host: "localhost",
Port: 5432,
Debug: true,
MaxConn: 10,
Timeout: 30 * time.Second,
}
// For logging config at startup
fmt.Printf("Config: %+v\n", cfg)
// Config: {Host:localhost Port:5432 Debug:true MaxConn:10 Timeout:30s}
// For debugging — display with types
fmt.Printf("Config detail: %#v\n", cfg)
// Config detail: main.Config{Host:"localhost", Port:5432, Debug:true, MaxConn:10, Timeout:30000000000}
Terminal Table Output #
func printTable(headers []string, rows [][]string) {
// Calculate the column widths
widths := make([]int, len(headers))
for i, h := range headers {
widths[i] = len(h)
}
for _, row := range rows {
for i, cell := range row {
if i < len(widths) && len(cell) > widths[i] {
widths[i] = len(cell)
}
}
}
// Format string for each column
formatStr := ""
separator := ""
for _, w := range widths {
formatStr += fmt.Sprintf("%%-%ds ", w)
separator += strings.Repeat("-", w+2)
}
formatStr += "\n"
// Print the header
headerArgs := make([]any, len(headers))
for i, h := range headers {
headerArgs[i] = h
}
fmt.Printf(formatStr, headerArgs...)
fmt.Println(separator)
// Print the rows
for _, row := range rows {
args := make([]any, len(row))
for i, cell := range row {
args[i] = cell
}
fmt.Printf(formatStr, args...)
}
}
// Usage
printTable(
[]string{"Name", "Email", "Role"},
[][]string{
{"Budi Santoso", "[email protected]", "Admin"},
{"Ani", "[email protected]", "User"},
{"Charlie Brown", "[email protected]", "Moderator"},
},
)
Performance: When Not to Use fmt #
The fmt package is convenient but not the fastest. For very performance-sensitive code, there are more efficient alternatives.
flowchart TD
A{"What do you\nwant to do?"} --> B{Output destination?}
B -- "Print to screen / stderr" --> P["fmt.Print*\nfmt.Fprintf(os.Stderr, ...)"]
B -- "Print to file / HTTP / buffer" --> FP["fmt.Fprintf\n(io.Writer)"]
B -- "Create a formatted string" --> C{"How complex\nis the format?"}
C -- "Many components\nor special formats" --> SP["fmt.Sprintf"]
C -- "Type conversion only\n(int→string, etc.)" --> SC["strconv.Itoa\nstrconv.FormatFloat\nstrconv.FormatBool"]
C -- "Join many strings\nin a loop" --> SB["strings.Builder\nwith sb.WriteString"]
A --> D{Creating an error?}
D -- "With context,\nunwrappable" --> EW["fmt.Errorf\nwith %w"]
D -- "A message only,\nno wrapping" --> EN["errors.New"]
style P fill:#e8f5e9,stroke:#4caf50
style FP fill:#e8f5e9,stroke:#4caf50
style SP fill:#e3f2fd,stroke:#2196f3
style SC fill:#fff3e0,stroke:#ff9800
style SB fill:#fff3e0,stroke:#ff9800
style EW fill:#fce4ec,stroke:#e91e63
style EN fill:#fce4ec,stroke:#e91e63import (
"strconv"
"strings"
)
// ANTI-PATTERN: Sprintf for simple conversions — extra allocation
func intToStringSlow(n int) string {
return fmt.Sprintf("%d", n) // format string allocation + conversion
}
// CORRECT: strconv for type conversions — faster and zero-allocation
func intToStringFast(n int) string {
return strconv.Itoa(n) // or strconv.FormatInt(int64(n), 10)
}
// ANTI-PATTERN: Sprintf for simple string concatenation
func makeKey1(prefix string, id int) string {
return fmt.Sprintf("%s:%d", prefix, id)
}
// CORRECT: strings.Builder for concatenating many strings
func makeKey2(prefix string, id int) string {
var sb strings.Builder
sb.WriteString(prefix)
sb.WriteByte(':')
sb.WriteString(strconv.Itoa(id))
return sb.String()
}
A practical guide to when to use what:
| Need | Use |
|---|---|
| Debugging, logging, console output | fmt.Printf / fmt.Sprintf |
| int/float to string conversion | strconv.Itoa / strconv.FormatFloat |
| string to int/float conversion | strconv.Atoi / strconv.ParseFloat |
| Concatenating many strings | strings.Builder |
| Creating errors with context | fmt.Errorf with %w |
| Output to a file/HTTP | fmt.Fprintf |
Benchmarks showstrconv.Itoais about 3-5x faster thanfmt.Sprintf("%d", n)for integer-to-string conversion. For hot paths in high-throughput applications (for example HTTP handlers called thousands of times per second), usestrconvinstead offmt.
When to Switch to Alternatives #
Keep using fmt if:
✓ Output to the console, stderr, or an io.Writer
✓ Creating strings with complex formats and many components
✓ Debugging with %+v, %#v, %T
✓ Creating errors with context using Errorf + %w
✓ Implementing Stringer for custom types
Consider strconv if:
✗ Simple type conversions (int↔string, float↔string, bool↔string)
✗ Hot path code needing maximum performance
✗ Parsing numeric strings from input or files
Consider strings.Builder if:
✗ Concatenating many strings in a loop
✗ Building large strings incrementally
Consider text/template or html/template if:
✗ Templates separated from code (.html, .txt files)
✗ HTML output needing auto-escaping for security
✗ Templates that can change without recompiling
Summary #
- Three main families:
Print*(to stdout),Fprint*(to an io.Writer),Sprint*(to a string) — the consistent naming pattern makes them easy to predict.- The
%vverb is a safe choice for all types;%+vfor structs with field names;%#vfor the Go syntax representation, very useful when debugging.- Width and precision control with the
%[width].[precision][verb]format — e.g.%8.2ffor a float with width 8 and 2 decimals,%-10sfor a left-aligned string of width 10.- Implement
Stringer(theString() stringmethod) for custom types so they display informatively when printed — this is a highly recommended Go convention.fmt.Errorfwith%wis the idiomatic way to create errors with context — use%w(not%v) soerrors.Isanderrors.Ascan work.- Error naming convention:
"functionName: detail"or"functionName arguments: %w"— this consistency makes error messages easy to trace when they occur in production.fmt.Fprintfto anio.Writermakes it very flexible — files, buffers, HTTP responses, network connections, all can be output destinations without changing the formatting code.- For high performance, avoid
fmt.Sprintffor simple conversions — usestrconvfor type conversions andstrings.Builderfor concatenating many strings.