53. Formatting and conversion: `fmt` and `strconv`
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.Sprintfis the workhorse.Fprint...— to anio.Writeryou 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.