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.

FamilyFunctionsPurpose
PrintPrint, Println, PrintfPrint to os.Stdout
FprintFprint, Fprintln, FprintfPrint to an io.Writer (file, buffer, stderr, etc.)
SprintSprint, Sprintln, SprintfReturn as a string
ScanScan, Scanln, ScanfRead input from os.Stdin
FscanFscan, Fscanln, FscanfRead input from an io.Reader
SscanSscan, Sscanln, SscanfRead input from a string
ErrorfErrorfCreate 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:#e91e63
package 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:#fce4ec

General 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:#fff
import (
    "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.Scan and fmt.Scanln aren’t suitable for input containing spaces because Scan splits values by whitespace. To read a full line including spaces, use bufio.Scanner or bufio.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:#e91e63
import (
    "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:

NeedUse
Debugging, logging, console outputfmt.Printf / fmt.Sprintf
int/float to string conversionstrconv.Itoa / strconv.FormatFloat
string to int/float conversionstrconv.Atoi / strconv.ParseFloat
Concatenating many stringsstrings.Builder
Creating errors with contextfmt.Errorf with %w
Output to a file/HTTPfmt.Fprintf
Benchmarks show strconv.Itoa is about 3-5x faster than fmt.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), use strconv instead of fmt.

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 %v verb is a safe choice for all types; %+v for structs with field names; %#v for the Go syntax representation, very useful when debugging.
  • Width and precision control with the %[width].[precision][verb] format — e.g. %8.2f for a float with width 8 and 2 decimals, %-10s for a left-aligned string of width 10.
  • Implement Stringer (the String() string method) for custom types so they display informatively when printed — this is a highly recommended Go convention.
  • fmt.Errorf with %w is the idiomatic way to create errors with context — use %w (not %v) so errors.Is and errors.As can work.
  • Error naming convention: "functionName: detail" or "functionName arguments: %w" — this consistency makes error messages easy to trace when they occur in production.
  • fmt.Fprintf to an io.Writer makes it very flexible — files, buffers, HTTP responses, network connections, all can be output destinations without changing the formatting code.
  • For high performance, avoid fmt.Sprintf for simple conversions — use strconv for type conversions and strings.Builder for concatenating many strings.

← Previous: Math   Next: Os →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact