Converting with strconv

Text from files, forms, command-line arguments and environment variables arrives as strings, and numbers have to become strings again to be displayed or saved. The strconv package ("string conversion") does both directions precisely, with control over number bases and sizes and clear errors when text isn't a valid number.

Integers: Atoi and Itoa

For plain decimal int values, use Atoi ("ASCII to integer") and Itoa:

GO
package main

import (
	"fmt"
	"strconv"
)

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

	s := strconv.Itoa(404)
	fmt.Println("Error " + s)

	for _, input := range []string{"-17", " 8", "3.5", "", "99999999999999999999"} {
		v, err := strconv.Atoi(input)
		fmt.Printf("%q -> %d, %v\n", input, v, err)
	}
}
Output
2027 <nil>
Error 404
"-17" -> -17, <nil>
" 8" -> 0, strconv.Atoi: parsing " 8": invalid syntax
"3.5" -> 0, strconv.Atoi: parsing "3.5": invalid syntax
"" -> 0, strconv.Atoi: parsing "": invalid syntax
"99999999999999999999" -> 9223372036854775807, strconv.Atoi: parsing "99999999999999999999": value out of range

Parsing is strict: surrounding spaces, decimal points and empty strings are errors, so trim user input first with strings.TrimSpace. On error the value is 0, except for out-of-range numbers, which come back as the largest (or smallest) value that fits.

Other bases and sizes: ParseInt and FormatInt

ParseInt(s, base, bitSize) handles any base from 2 to 36 and checks the result fits in the given number of bits. Base 0 means "work it out from the prefix" (0x, 0o, 0b), which also allows underscores. ParseUint is the unsigned version. The result is always an int64 (or uint64), which you then convert:

GO
package main

import (
	"fmt"
	"strconv"
)

func main() {
	hex, _ := strconv.ParseInt("ff", 16, 64)
	bin, _ := strconv.ParseInt("1010", 2, 64)
	auto, _ := strconv.ParseInt("0x1F", 0, 64)
	fmt.Println(hex, bin, auto)

	_, err := strconv.ParseInt("300", 10, 8) // must fit in an int8
	fmt.Println(err)

	port, err := strconv.ParseUint("8080", 10, 16)
	fmt.Println(uint16(port), err)

	fmt.Println(strconv.FormatInt(255, 2), strconv.FormatInt(255, 16), strconv.FormatInt(-35, 36))
}
Output
255 10 31
strconv.ParseInt: parsing "300": value out of range
8080 <nil>
11111111 ff -z

Floats

GO
package main

import (
	"fmt"
	"strconv"
)

func main() {
	f, err := strconv.ParseFloat("3.14159", 64)
	fmt.Println(f*2, err)

	for _, s := range []string{"1e3", "-0.5", "inf", "NaN", "1,5"} {
		v, err := strconv.ParseFloat(s, 64)
		fmt.Println(s, "->", v, err)
	}

	x := 2.0 / 3
	fmt.Println(strconv.FormatFloat(x, 'f', 2, 64))
	fmt.Println(strconv.FormatFloat(x, 'f', -1, 64)) // -1: as few digits as needed
	fmt.Println(strconv.FormatFloat(1234567.0, 'e', 3, 64))
}
Output
6.28318 <nil>
1e3 -> 1000 <nil>
-0.5 -> -0.5 <nil>
inf -> +Inf <nil>
NaN -> NaN <nil>
1,5 -> 0 strconv.ParseFloat: parsing "1,5": invalid syntax
0.67
0.6666666666666666
1.235e+06

Go always uses a dot as the decimal separator; strings like "1,5" from some locales must be converted first. FormatFloat's format byte works like the Printf verbs: 'f', 'e' or 'g'.

Booleans

GO
package main

import (
	"fmt"
	"strconv"
)

func main() {
	for _, s := range []string{"true", "1", "T", "FALSE", "yes"} {
		b, err := strconv.ParseBool(s)
		fmt.Println(s, b, err)
	}
	fmt.Println(strconv.FormatBool(true))
}
Output
true true <nil>
1 true <nil>
T true <nil>
FALSE false <nil>
yes false strconv.ParseBool: parsing "yes": invalid syntax
true

Quoting

Quote turns a string into a Go string literal with escapes; Unquote reverses it. This is the same formatting %q uses:

GO
package main

import (
	"fmt"
	"strconv"
)

func main() {
	q := strconv.Quote("tab\there \"quoted\"")
	fmt.Println(q)
	s, err := strconv.Unquote(q)
	fmt.Println(s, err)
	fmt.Println(strconv.QuoteToASCII("café"))
}
Output
"tab\there \"quoted\""
tab	here "quoted" <nil>
"caf\u00e9"

Inspecting errors

The errors from strconv are *strconv.NumError values that record the function, the input and the reason. You can test the reason with errors.Is (lesson 45):

GO
package main

import (
	"errors"
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.ParseInt("128", 10, 8)
	if errors.Is(err, strconv.ErrRange) {
		fmt.Println("too big for an int8")
	}
	if numErr, ok := errors.AsType[*strconv.NumError](err); ok {
		fmt.Println(numErr.Func, numErr.Num)
	}
}
Output
too big for an int8
ParseInt 128

Appending without allocating

The Append… functions write a number's text onto the end of a byte slice — useful in performance-sensitive code that builds output in a buffer:

GO
package main

import (
	"fmt"
	"strconv"
)

func main() {
	buf := []byte("id=")
	buf = strconv.AppendInt(buf, 42, 10)
	buf = append(buf, "&ok="...)
	buf = strconv.AppendBool(buf, true)
	fmt.Println(string(buf))
}
Output
id=42&ok=true

strconv or fmt?

fmt.Sprint(n) and fmt.Sscan also convert between numbers and text, and they're convenient when you're formatting anyway. strconv is faster, stricter and gives better errors, so it's the better choice for parsing input.

Try it yourself

Parse settings given as text, using a default when a value is missing or invalid.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
workers: 8
retries: 3
timeout: 30
Next: building strings efficiently.