53. Formatting and conversion: `fmt` and `strconv`

📖 Reading · 11 min
💡 Every code box below is live — edit it and hit Run.

Two packages you'll import in almost every file. fmt turns values into text for humans; strconv converts between strings and numbers exactly and with errors. Knowing which is which saves a lot of guessing.

The fmt verbs worth memorising

package main

import "fmt"

type Point struct{ X, Y int }

func main() {
    p := Point{3, 4}
    s := "go"
    n := 42
    f := 3.14159
    b := true

    fmt.Printf("%v  %+v\n", p, p)
    fmt.Printf("%d  %5d  %-5d|\n", n, n, n)
    fmt.Printf("%f  %.2f  %8.2f\n", f, f, f)
    fmt.Printf("%s  %q  %10s|\n", s, s, s)
    fmt.Printf("%t  %v\n", b, b)
    fmt.Printf("%b %o %x %X\n", n, n, n, n)
    fmt.Printf("%e\n", 1234567.0)
    fmt.Printf("%%literal percent\n")
}

The short list:

verb for
%v anything — the default format
%+v structs, with field names (the debugging one)
%d integers
%f / %.2f floats, with precision
%s strings
%q quoted string — shows whitespace and escapes
%t booleans
%T the value's type
%x / %b / %o hex / binary / octal

Width and alignment go before the verb: %5d pads to 5, %-10s left-aligns in 10. That's how you produce a table without a library:

package main

import "fmt"

type Item struct {
    Name  string
    Qty   int
    Price float64
}

func main() {
    items := []Item{
        {"Keyboard", 12, 49.99},
        {"Monitor", 3, 199.5},
        {"USB-C cable", 87, 9.99},
    }

    fmt.Printf("%-14s %5s %10s\n", "ITEM", "QTY", "PRICE")
    for _, it := range items {
        fmt.Printf("%-14s %5d %10.2f\n", it.Name, it.Qty, it.Price)
    }
}

Print, Sprint, Fprint

Every fmt function comes in three flavours, and the prefix tells you where the output goes:

package main

import (
    "bytes"
    "fmt"
    "os"
)

func main() {
    fmt.Println("Print* -> standard output")

    s := fmt.Sprintf("Sprint* -> a string (%d chars)", 42)
    fmt.Println(s)

    var buf bytes.Buffer
    fmt.Fprintf(&buf, "Fprint* -> any io.Writer")
    fmt.Println(buf.String())

    fmt.Fprintln(os.Stdout, "which includes os.Stdout")
    fmt.Fprintln(os.Stderr, "and os.Stderr for diagnostics")
}
  • Print... — to standard output.
  • Sprint... — returns a string. Sprintf is the workhorse.
  • Fprint... — to an io.Writer you choose (module 6).

And the ln/f suffixes: Println adds spaces between arguments plus a newline; Printf takes a format string and adds nothing you didn't ask for.

strconv: strings ↔ numbers, exactly

fmt is for presentation. When you're parsing input or generating exact output, use strconv:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    n, err := strconv.Atoi("42")
    fmt.Println(n+1, err)

    _, err = strconv.Atoi("42.5")
    fmt.Println("error:", err)

    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Println(f * 2)

    b, _ := strconv.ParseBool("true")
    fmt.Println(!b)

    hex, _ := strconv.ParseInt("ff", 16, 64)
    fmt.Println(hex)

    fmt.Println(strconv.Itoa(99) + "!")
    fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64))
    fmt.Println(strconv.Quote("line\twith\ttabs"))
}

The names decode as: ASCII to integer, integer to ASCII, and Parse.../Format... for everything else.

Why not just use fmt.Sprintf("%d", n)? strconv.Itoa is several times faster and says exactly what it does. And in the other direction it isn't even close — strconv.Atoi returns an error for bad input, which fmt.Sscanf makes much harder to get right.

Rule: fmt to display, strconv to convert.

ParseInt's bit size

package main

import (
    "fmt"
    "strconv"
)

func main() {
    small, err := strconv.ParseInt("300", 10, 8)
    fmt.Println(small, err)

    ok, _ := strconv.ParseInt("300", 10, 16)
    fmt.Println(ok)

    big, _ := strconv.ParseInt("9000000000", 10, 64)
    fmt.Println(big)
}

The third argument is how many bits the result must fit in — 8, 16, 32 or 64, with 0 meaning int. Ask for 8 bits and 300 is a range error, caught at parse time rather than silently truncated. This is Go being strict on your behalf.

Custom formatting with String()

From module 6, but it belongs in this list: implement String() string and the string verbs — %v, %s, and the Print family — use it. The verb still decides: %d on the same value prints the number, not the name.

package main

import (
    "fmt"
    "strings"
)

type Level int

const (
    Debug Level = iota
    Info
    Warn
    Error
)

func (l Level) String() string {
    names := []string{"DEBUG", "INFO", "WARN", "ERROR"}
    if int(l) < 0 || int(l) >= len(names) {
        return fmt.Sprintf("Level(%d)", int(l))
    }
    return names[l]
}

func main() {
    for _, l := range []Level{Debug, Info, Warn, Error, Level(9)} {
        fmt.Printf("%-6v %s\n", l, strings.Repeat("=", int(l)+1))
    }
}

A String() on an iota constant type turns unreadable integers into names everywhere they're printed — logs, errors, tests. Fifteen lines that pay for themselves immediately.

Common formatting mistakes

package main

import "fmt"

func main() {
    fmt.Printf("two numbers: %d and %d\n", 1)

    fmt.Printf("just one: %d\n", 1, 2)

    fmt.Printf("wrong verb: %d\n", "a string")

    fmt.Println("Println needs no verbs:", 1, "and", 2)
}

Look at the first three output lines — %!d(MISSING), %!(EXTRA int=2) and %!d(string=a string). fmt never panics on a bad format; it prints a marker inline. That's helpful in production and easy to miss in a log, so run go vet — it catches every one of these at build time. Module 12 has more on that.

Your turn

Parse the two strings, add them, and print the result to two decimal places:

7.75
package main

import (
    "fmt"
    "strconv"
)

func main() {
    a := "5"
    b := "2.75"
    // parse a as an int and b as a float64, add them, print with 2 decimals
}
package main

import (
    "fmt"
    "strconv"
)

func main() {
    a := "5"
    b := "2.75"

    n, err := strconv.Atoi(a)
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    f, err := strconv.ParseFloat(b, 64)
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    fmt.Printf("%.2f\n", float64(n)+f)
}

Next: the package everyone gets wrong at first — time.