fmt, Strings and Text Processing

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27. Almost every Go program formats, parses, searches or tokenizes text, and almost all of that is done by a handful of standard-library packages that fit together: fmt turns values into text and text into values, strings and bytes slice and search text that is already in memory, strconv converts between numbers and their textual form, unicode classifies runes, regexp matches patterns in guaranteed linear time, and text/scanner, bufio.Scanner and text/tabwriter tokenize and lay out streams. This page covers all of them: the full fmt verb language and the interfaces (Stringer, GoStringer, Formatter) that customize it, the scanning functions, the whole strings API including the iterator functions and the new strings.CutLast of Go 1.27, bytes.Buffer, strconv and its error values, unicode and Unicode 17, the RE2 dialect of regexp, and the compile errors and go vet reports that each package produces.

It builds on Strings, Runes and UTF-8, which explains what a string, a byte and a rune are, and it is the text counterpart of I/O, Files and the Filesystem, where io.Reader, io.Writer and bufio readers and writers are covered in depth. Generating documents from templates is the subject of Text and HTML Templates, JSON is covered in encoding/json, dates and durations in Time and Timers, and structured log lines in Structured Logging with slog. How errors are wrapped and inspected is in Error Handling; this page only shows the formatting side of %w. The same ground in other languages is covered by the Rust, C, Java and Python references.

Every example below was compiled, vetted with go vet and run with Go 1.27.2; the output shown is the real output (error demonstrations are marked as such). They all live in one module whose go.mod is:

module example.com/shelf

go 1.27

Which package for which job

Task Package and functions Notes

Format values as text

fmt.Sprintf, fmt.Fprintf, fmt.Appendf

Verbs, flags, width and precision; Stringer customizes %v and %s.

Parse text that follows a fixed layout

fmt.Sscanf, fmt.Fscan

Convenient, forgiving about nothing; prefer strings.Cut and strconv for anything formal.

Split, join, search, trim, replace

strings (and bytes for []byte)

Everything is byte-oriented, UTF-8 aware where it needs to be.

Build a string piece by piece

strings.Builder

Avoids the quadratic copying of s += x in a loop.

Numbers to and from text

strconv.Atoi, ParseInt, ParseFloat, FormatInt, Quote

Returns *strconv.NumError wrapping ErrSyntax or ErrRange.

Character classes and scripts

unicode, unicode/utf8

Unicode 17 tables since Go 1.27.

Pattern matching with captures

regexp

RE2 syntax: no backreferences or lookaround, but linear time in the input.

Tokenize source-like text

text/scanner

Go-flavoured lexer for identifiers, numbers, strings and comments.

Read lines, words or custom records

bufio.Scanner

64 KiB token limit unless you raise it with Buffer.

Align columns

text/tabwriter

Elastic tab stops; counts runes, not display cells.

fmt: formatted input and output

The fmt package implements formatted I/O in the spirit of C’s printf and scanf, with a format language that is simpler and safer because Go values carry their types at run time. Every function takes the operands as ...any, so the verb can inspect the real type, and a wrong combination produces readable text in the output instead of undefined behaviour. The formatting functions come in four families with three flavours each.

The print families

Flavour Write to standard output Return a string Write to an io.Writer

default formatting (%v for each operand)

Print

Sprint

Fprint

format string

Printf

Sprintf

Fprintf

default formatting, spaces always, newline at the end

Println

Sprintln

Fprintln

A fourth family, Append, Appendf and Appendln (Go 1.19), formats into a caller-supplied []byte and returns the extended slice, which avoids allocating an intermediate string when you are building a buffer anyway. Print, Printf and Println are Fprint* with os.Stdout, and they return the number of bytes written and any write error; most programs ignore those results, and go vet does not complain. Fprint* to os.Stderr is the usual way to report problems. The only difference between Print and Println is spacing: Print adds a space between two operands only when neither is a string, while Println always adds a space and a final newline.

package main

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

func main() {
	// Print adds spaces between operands when neither is a string.
	fmt.Print("shelf", 3, 4, "books", "\n")
	fmt.Print(3, 4, "\n")
	// Println always separates operands with a space and appends a newline.
	fmt.Println("shelf", 3, 4, "books")

	// Sprint* return a string, Fprint* write to an io.Writer,
	// Append* append to a []byte and return the extended slice.
	s := fmt.Sprintf("%s has %d copies", "Dune", 3)
	fmt.Println(s)

	n, err := fmt.Fprintf(os.Stdout, "%-6s|\n", "Dune")
	fmt.Println("wrote", n, "bytes, err =", err)

	var sb strings.Builder
	fmt.Fprintf(&sb, "%03d-%s", 7, "x")
	fmt.Println(sb.String())

	var buf bytes.Buffer
	fmt.Fprintln(&buf, "line one")
	fmt.Fprint(&buf, "line", " ", "two")
	fmt.Printf("%q\n", buf.String())

	b := make([]byte, 0, 32)
	b = fmt.Append(b, "id=", 42)
	b = fmt.Appendf(b, " ok=%t", true)
	b = fmt.Appendln(b, " done")
	fmt.Printf("%q\n", b)

	// Fprintln to os.Stderr is the usual way to report problems.
	fmt.Fprintln(os.Stderr, "shelf: this goes to standard error")
}
shelf3 4books
3 4
shelf 3 4 books
Dune has 3 copies
Dune  |
wrote 8 bytes, err = <nil>
007-x
"line one\nline two"
"id=42 ok=true done\n"
shelf: this goes to standard error

Verbs

A format string contains ordinary text, copied unchanged, and directives that start with %. A directive has the form %[flags][width][.precision][argument index]verb. The verb decides how the operand is rendered. The general verbs work on every type:

Verb Meaning

%v

The operand in its default format. Structs print as {f1 f2}, pointers to structs as &{f1 f2}, maps as map[k:v] with sorted keys, nil as <nil>.

%+v

Like %v but struct field names are included: {Name:Ada}.

%#v

A Go-syntax representation that would compile: main.Book{Title:"Dune"}, []int(nil), map[string]int(nil).

%T

The Go-syntax type of the operand: int, []interface {}, main.Book.

%%

A literal percent sign; consumes no operand.

The remaining verbs depend on the kind of operand:

Operand Verbs Meaning

boolean

%t

true or false.

integer

%d %b %o %O %x %X

Base 10, 2, 8, 8 with 0o prefix, 16 with lower-case or upper-case digits.

integer

%c %q %U

The character with that code point, the single-quoted Go literal for it, and U+0041 notation (%#U adds the character).

float and complex

%e %E %f %F %g %G %b %x %X

Scientific, decimal point, and the shortest of the two (%g, the default for %v); %b and %x are exact binary and hexadecimal forms.

string and []byte

%s %q %x %X

The bytes as text, a double-quoted Go literal with escapes, and two hexadecimal digits per byte.

slice

any element verb, plus %p

The verb is applied to every element: [%03d] on []int{1,2} gives [001 002].

pointer

%p, %v

Hexadecimal address; %v of a pointer to a struct, array, slice or map shows & and the contents one level deep only.

error or Stringer

%v %s %q %x %X

The Error() or String() text, formatted as a string.

Flags and modifiers go between the % and the verb:

Modifier Effect

+

Always print a sign for numbers; with %q guarantee ASCII output; with %v on structs, add field names (the same as %+v).

-

Pad on the right (left-justify) instead of the left.

#

Alternate format: 0x/0 prefix for hexadecimal and octal, 0b for %#b, a raw backquoted string for %#q when possible, Go syntax for %#v.

space

Leave a space for the sign of numbers (% d), and put spaces between the bytes of % x on strings and slices.

0

Pad with leading zeros rather than spaces; for numbers the padding goes after the sign.

width

A minimum number of runes (not bytes) for the whole field, counted in runes for strings: %10s, %-6d.

.precision

For floats the digits after the decimal point (%.2f) or the number of significant digits for %g; for strings and byte slices the maximum number of runes (%.3s); for integers the minimum number of digits.

*

Take the width or precision from the next int argument: %*d and %.*f.

[n]

An explicit argument index (1-based) before a verb or a *: %[2]s %[1]s, %[3]*.[2]*[1]f.

The first example shows the general verbs on a Book that is deliberately a bit ugly (a pointer, a map, a nested slice), because that is where the verbs differ.

package main

import (
	"fmt"
	"time"
)

type ISBN string

type Author struct{ Name string }

type Book struct {
	ISBN    ISBN
	Title   string
	Authors []Author
	Pages   int
	Rating  *float64
	Tags    map[string]bool
}

func main() {
	rating := 4.5
	b := Book{
		ISBN:    "9780441172719",
		Title:   "Dune",
		Authors: []Author{{"Frank Herbert"}},
		Pages:   412,
		Rating:  &rating,
		Tags:    map[string]bool{"scifi": true, "classic": true},
	}
	b2 := b
	b2.Rating = nil

	fmt.Printf("%%v   %v\n", b2)
	fmt.Printf("%%+v  %+v\n", b2)
	fmt.Printf("%%#v  %#v\n", b2)
	fmt.Printf("%%T   %T\n", b2)
	fmt.Printf("%%T   %T %T %T %T\n", 1, "s", 2.5, []any{nil})
	fmt.Printf("%%v of a pointer to struct: %v\n", &Author{"Ursula K. Le Guin"})
	fmt.Printf("%%+v of a pointer to struct: %+v\n", &Author{"Ursula K. Le Guin"})
	fmt.Printf("%%v of nil pointer, error, map, slice: %v %v %v %v\n", (*int)(nil), error(nil), map[string]int(nil), []int(nil))
	fmt.Printf("%%#v of those: %#v %#v %#v %#v\n", (*int)(nil), error(nil), map[string]int(nil), []int(nil))
	fmt.Printf("%%v of a duration and a time: %v | %v\n", 90*time.Minute, time.Date(2026, 10, 18, 9, 30, 0, 0, time.UTC))
	fmt.Printf("%%v of a byte slice and a string slice: %v %v\n", []byte("hi"), []string{"a", "b c"})
	fmt.Printf("%%q of the string slice: %q\n", []string{"a", "b c"})
	fmt.Printf("%%d %%x of the byte slice: %d %x\n", []byte("hi"), []byte("hi"))
}
%v   {9780441172719 Dune [{Frank Herbert}] 412 <nil> map[classic:true scifi:true]}
%+v  {ISBN:9780441172719 Title:Dune Authors:[{Name:Frank Herbert}] Pages:412 Rating:<nil> Tags:map[classic:true scifi:true]}
%#v  main.Book{ISBN:"9780441172719", Title:"Dune", Authors:[]main.Author{main.Author{Name:"Frank Herbert"}}, Pages:412, Rating:(*float64)(nil), Tags:map[string]bool{"classic":true, "scifi":true}}
%T   main.Book
%T   int string float64 []interface {}
%v of a pointer to struct: &{Ursula K. Le Guin}
%+v of a pointer to struct: &{Name:Ursula K. Le Guin}
%v of nil pointer, error, map, slice: <nil> <nil> map[] []
%#v of those: (*int)(nil) <nil> map[string]int(nil) []int(nil)
%v of a duration and a time: 1h30m0s | 2026-10-18 09:30:00 +0000 UTC
%v of a byte slice and a string slice: [104 105] [a b c]
%q of the string slice: ["a" "b c"]
%d %x of the byte slice: [104 105] 6869

Source: fmt: Printing

Three details in that output are worth remembering. A map is always printed with its keys sorted (since Go 1.12), so %v output is deterministic and safe to compare in tests, unlike iteration with range, which is randomized (see Maps). A pointer to a struct prints as &{...}, but a pointer nested inside a struct, such as the Rating field above, prints as an address or <nil>: fmt follows only the top-level pointer. And %#v is the only verb that distinguishes a nil slice or map ([]int(nil)) from an empty one ([]int{}), which makes it the right choice for test failure messages.

Flags, width, precision and argument indexes

package main

import "fmt"

func main() {
	// integers
	n := 255
	fmt.Printf("[%d] [%5d] [%-5d] [%05d] [%+d] [% d]\n", n, n, n, n, n, n)
	fmt.Printf("[%b] [%o] [%O] [%x] [%X] [%#x] [%#o] [%#b]\n", n, n, n, n, n, n, n, n)
	fmt.Printf("[%c] [%U] [%#U] [%q]\n", 'é', 'é', 'é', 'é')
	fmt.Printf("[%08.3f] [%+.2e] [%g] [%G]\n", 3.14159, 12345.678, 1e21, 1e-7)
	fmt.Printf("[%6.2f] [%-8.1f] [%.0f] [%.3g]\n", 3.14159, 2.5, 2.5, 1234.5678)
	fmt.Printf("[%v] [%v] [%v]\n", 1e6, 1e21, float32(0.1))

	// strings
	s := "Dune"
	fmt.Printf("[%s] [%10s] [%-10s] [%.2s] [%10.2s] [%q] [%x] [% x] [%X]\n", s, s, s, s, s, s, s, s, s)
	fmt.Printf("[%5t] [%-7t]\n", true, false)

	// width and precision from arguments
	fmt.Printf("[%*d] [%-*d] [%.*f]\n", 6, 42, 6, 42, 2, 3.14159)

	// explicit argument indexes
	fmt.Printf("%[2]s %[1]s %[2]s\n", "world", "hello")
	fmt.Printf("%[1]d in binary is %[1]b and in hex is %#[1]x\n", 42)
	fmt.Printf("%[3]*.[2]*[1]f|\n", 12.0, 2, 8)

	// a table, the usual use of width
	rows := []struct {
		Title string
		Pages int
		Price float64
	}{{"Dune", 412, 9.99}, {"Solaris", 204, 7.5}, {"Neuromancer", 271, 8.25}}
	fmt.Printf("%-12s %5s %8s\n", "Title", "Pages", "Price")
	for _, r := range rows {
		fmt.Printf("%-12s %5d %8.2f\n", r.Title, r.Pages, r.Price)
	}

	// width counts runes, not bytes, for strings
	fmt.Printf("[%-6s] [%-6s]\n", "日本", "ab")
}
[255] [  255] [255  ] [00255] [+255] [ 255]
[11111111] [377] [0o377] [ff] [FF] [0xff] [0377] [0b11111111]
[é] [U+00E9] [U+00E9 'é'] ['é']
[0003.142] [+1.23e+04] [1e+21] [1E-07]
[  3.14] [2.5     ] [2] [1.23e+03]
[1e+06] [1e+21] [0.1]
[Dune] [      Dune] [Dune      ] [Du] [        Du] ["Dune"] [44756e65] [44 75 6e 65] [44756E65]
[ true] [false  ]
[    42] [42    ] [3.14]
hello world hello
42 in binary is 101010 and in hex is 0x2a
   12.00|
Title        Pages    Price
Dune           412     9.99
Solaris        204     7.50
Neuromancer    271     8.25
[日本    ] [ab    ]

Some rules from that output that surprise newcomers:

  • %x on an integer formats the number; on a string or []byte it formats every byte, and % x inserts a space between bytes. %X gives upper-case digits. Both %x and %q on a type that implements Stringer or error format the string returned by the method, not the underlying integer (see below).

  • Width is a minimum and counts runes, so %-6s of "日本" pads with four spaces. It does not measure display cells: East Asian wide characters and combining marks make columns look misaligned in a terminal.

  • A precision on a string truncates it (%.2s of "Dune" is Du); on a float it rounds (%.0f of 2.5 is 2, because the exact binary value is ties-to-even). %g with a precision counts significant digits, and %v of a float64 uses %g with the shortest representation that round-trips (1e+06, 1e+21).

  • Money should not be stored in a float64 just because %.2f makes it look right; use integer cents and format them yourself (see the Formatter example below).

  • %+d prints a sign even for positive numbers, % d leaves a space for it, and %08.3f zero-pads a float to eight characters in total including the dot.

Wrong verbs and wrong argument counts

fmt never panics because of a mismatch between the format and the operands. Instead it writes a diagnostic into the output, which is easy to miss in logs but searchable once you know the pattern:

Output fragment Cause

%!d(string=Dune)

The verb %d does not apply to a string.

%!d(MISSING)

The format has more verbs than the call has operands.

%!(EXTRA int=2)

The call has more operands than the format has verbs.

%!z(int=1)

Unknown verb z.

%!(BADWIDTH) and %!(BADPREC)

The operand for * is not an int.

%!d(BADINDEX)

An explicit argument index is out of range.

%!s(<nil>)

A nil interface operand was given to a verb that needs a value.

%!v(PANIC=String method: ...)

The operand’s String or Error method panicked; fmt recovers and reports it.

%!(NOVERB)

The format string ends in a lone %.

package main

import "fmt"

func main() {
	fmt.Println(fmt.Sprintf("%d", "Dune"))
	fmt.Println(fmt.Sprintf("%s", 42))
	fmt.Println(fmt.Sprintf("%d %d", 1))
	fmt.Println(fmt.Sprintf("%d", 1, 2))
	fmt.Println(fmt.Sprintf("%z", 1))
	fmt.Println(fmt.Sprintf("%*d", "w", 1))
	fmt.Println(fmt.Sprintf("%[3]d", 1, 2))
	fmt.Println(fmt.Sprintf("%s %d", nil, nil))
}

Output of go run:

%!d(string=Dune)
%!s(int=42)
1 %!d(MISSING)
1%!(EXTRA int=2)
%!z(int=1)
%!(BADWIDTH)1
%!d(BADINDEX)
%!s(<nil>) %!d(<nil>)

go vet catches almost all of these at build time through its printf analyzer, which also understands your own wrapper functions when their name ends in f and they forward to a fmt function. go test runs the same analyzer, so a test run fails on such a call. The reports for the program above (this is an error demonstration, not part of the module you should keep) are:

e/badverbs/main.go:6:27: fmt.Sprintf format %d has arg "Dune" of wrong type string
e/badverbs/main.go:7:27: fmt.Sprintf format %s has arg 42 of wrong type int
e/badverbs/main.go:8:30: fmt.Sprintf format %d reads arg #2, but call has 1 arg
e/badverbs/main.go:9:14: fmt.Sprintf call needs 1 arg but has 2 args
e/badverbs/main.go:10:27: fmt.Sprintf format %z has unknown verb z
e/badverbs/main.go:11:27: fmt.Sprintf format %*d uses non-int "w" as argument of *
e/badverbs/main.go:12:27: fmt.Sprintf format %[3]d reads arg #3, but call has 2 args
e/badverbs/main.go:13:27: fmt.Sprintf format %s has arg nil of wrong type untyped nil

Pointers, nil, collections and a few more cases

package main

import (
	"fmt"
	"os"
)

type Node struct {
	Name string
	Next *Node
}

func main() {
	n := &Node{"a", nil}
	fmt.Printf("%v %+v\n", n, n) // only the top-level pointer is dereferenced
	m := &Node{"b", n}
	fmt.Println(m.Next == n, fmt.Sprintf("%v", m) == fmt.Sprintf("&{b %p}", n))
	fmt.Printf("%#v\n", *n)

	x := 42
	p := &x
	fmt.Println(p == &x, fmt.Sprintf("%p", p) == fmt.Sprint(p), fmt.Sprintf("%v", p)[:2])

	// Verbs on collections apply element by element.
	fmt.Printf("%v %03d %x %q\n", []int{1, 2}, []int{1, 2}, []string{"hi", "yo"}, []string{"hi", "yo"})
	fmt.Printf("%v %+v\n", map[string][]int{"b": {2}, "a": {1}}, struct {
		A int
		B *int
	}{1, nil})

	// Floating point verbs.
	f := 1234.5678
	fmt.Printf("%v %g %e %.1f %8.3f|%-8.1f|%x\n", f, f, f, f, f, f, 1.0)

	// Runes are integers: %d shows the number, %c the character.
	r := 'é'
	fmt.Printf("%d %c %q %U %x\n", r, r, r, r, r)
	fmt.Println(r, string(r))

	// Print vs Println spacing differences.
	fmt.Print("a", "b", 1, 2, true, "c", "\n")
	fmt.Print(1, 2, "\n")

	fmt.Fprintf(os.Stdout, "%6.2f%%\n", 45.678)
	var e error
	fmt.Printf("%v\n", e)
	var ip *int
	fmt.Printf("%v %d\n", ip, ip)
	var a any = 3
	fmt.Printf("%v %T %5v|%-5v|\n", a, a, a, a)
	type pair struct{ a, b any }
	fmt.Printf("%v %+v\n", pair{1, "x"}, pair{nil, []byte("y")})
}
&{a <nil>} &{Name:a Next:<nil>}
true true
main.Node{Name:"a", Next:(*main.Node)(nil)}
true true 0x
[1 2] [001 002] [6869 796f] ["hi" "yo"]
map[a:[1] b:[2]] {A:1 B:<nil>}
1234.5678 1234.5678 1.234568e+03 1234.6 1234.568|1234.6  |0x1p+00
233 é 'é' U+00E9 e9
233 é
ab1 2 truec
1 2
 45.68%
<nil>
<nil> 0
3 int     3|3    |
{1 x} {a:<nil> b:[121]}

Source: fmt: Printing

Notice that a rune is just an int32: Println prints its number (233), while %c, %q and string(r) print the character. The same is true of a byte, which prints as a number unless you ask for %c or convert a []byte to string. A nil error prints as <nil> with %v, so Println(err) is safe on success, but %s of a nil interface gives %!s(<nil>). Formatting a nil pointer with %v also gives <nil>, while %d of the same pointer prints 0.

Performance and where fmt does not belong

fmt uses reflection and a pooled internal buffer, which makes it convenient but not free. For a hot path that formats a number or joins a few strings, strconv.AppendInt, strings.Builder and plain concatenation of a few operands are faster and allocate less; for ordinary program output the difference is irrelevant, and readability wins. Two rules of thumb: do not call fmt.Sprintf("%s", s) or fmt.Sprint(s) to convert something that is already a string, and do not use Sprintf to build a SQL query or a shell command from user input; that is an injection bug, not a formatting question (see database/sql for placeholders).

Stringer, GoStringer and Formatter

fmt consults three interfaces, in this order of precedence, before falling back to reflection:

  1. Formatter (Format(f fmt.State, verb rune)) takes over completely for every verb.

  2. For the verbs that print strings (%v, %s, %q, %x, %X) and for Print/Println: if the operand implements error, Error() is used; otherwise if it implements Stringer (String() string), String() is used.

  3. For %#v: GoStringer (GoString() string) is used if present.

Stringer is defined in the fmt package, not in the language; the interfaces that are predeclared are only error, any and comparable. Implementing it is the idiomatic way to give a type a readable form, as the catalog.ISBN and catalog.LoanState types of the shelf module do:

package main

import (
	"errors"
	"fmt"
)

type ISBN string

func (i ISBN) String() string {
	if len(i) != 13 {
		return string(i)
	}
	return string(i[:3]) + "-" + string(i[3:4]) + "-" + string(i[4:])
}

type LoanState int

const (
	Available LoanState = iota
	OnLoan
)

func (s LoanState) String() string {
	switch s {
	case Available:
		return "available"
	case OnLoan:
		return "on loan"
	}
	return fmt.Sprintf("LoanState(%d)", int(s))
}

// GoString controls %#v.
func (s LoanState) GoString() string { return "catalog." + [...]string{"Available", "OnLoan"}[s] }

type Book struct {
	ISBN  ISBN
	Title string
	State LoanState
}

// Pointer receiver: only *Member values are Stringers.
type Member struct{ Name string }

func (m *Member) String() string { return "member " + m.Name }

// ErrNotFound is an error; fmt prefers Error over String.
type NotFound struct{ ISBN ISBN }

func (e NotFound) Error() string  { return "not found: " + string(e.ISBN) }
func (e NotFound) String() string { return "never used by fmt for %v" }

// Recursive formats a field, not the receiver, so it does not call itself.
type Recursive struct{ n int }

func (r Recursive) String() string { return fmt.Sprintf("%d", r.n) } // fine: converts the field, not r

type Panicky struct{ p *Member }

func (p Panicky) String() string { return p.p.Name }

func main() {
	b := Book{"9780441172719", "Dune", OnLoan}
	fmt.Println(b)
	fmt.Printf("%v | %+v | %#v\n", b, b, b)
	fmt.Printf("%s | %d | %x | %q\n", b.State, b.State, b.State, b.State)
	fmt.Printf("%s %v %q\n", b.ISBN, b.ISBN, b.ISBN)

	m := Member{"Ana"}
	fmt.Println(m, &m)
	fmt.Println([]*Member{&m}, []Member{m})

	var e error = NotFound{"9780441172719"}
	fmt.Println(e)
	fmt.Printf("%v %s\n", e, errors.Unwrap(e))

	fmt.Println(Recursive{7})
	fmt.Println(Panicky{})
	var np *Member
	fmt.Println(np)
}
{978-0-441172719 Dune on loan}
{978-0-441172719 Dune on loan} | {ISBN:978-0-441172719 Title:Dune State:on loan} | main.Book{ISBN:"9780441172719", Title:"Dune", State:catalog.OnLoan}
on loan | 1 | 6f6e206c6f616e | "on loan"
978-0-441172719 978-0-441172719 "978-0-441172719"
{Ana} member Ana
[member Ana] [{Ana}]
not found: 9780441172719
not found: 9780441172719 %!s(<nil>)
7
%!v(PANIC=String method: runtime error: invalid memory address or nil pointer dereference)
<nil>

The output teaches several rules at once:

  • Stringer applies to nested values too. The Book is printed as {978-0-441172719 Dune on loan}, with the ISBN and LoanState fields rendered by their String methods, because fmt calls the method on every exported field (or interface element) it reaches; it cannot call methods on unexported fields, which are printed raw. %d still prints the underlying number, while %x of a LoanState surprisingly hexadecimal-encodes the string returned by String (6f6e206c6f616e), since %x is a string verb as soon as the operand is a Stringer.

  • %#v ignores String and shows the Go syntax, unless GoString is defined, as it is here for LoanState.

  • Method sets matter. Member defines String on *Member, so fmt.Println(m) with a Member value prints {Ana}, but Println(&m) prints member Ana. A slice []*Member therefore uses the method and []Member does not. Prefer a value receiver for String unless the type must not be copied (see Methods and Receivers).

  • A type that implements both error and Stringer prints through Error(); the String method of NotFound is never used by %v.

  • If String panics, fmt catches the panic and prints %!v(PANIC=String method: ...). If the receiver is a nil pointer, it prints <nil> instead, because that case is so common that the package special-cases it.

The recursion trap

A String method that formats its own receiver with a string verb calls itself forever:

package main

import "fmt"

type LoanState int

// String is wrong: Sprintf with %s on the receiver calls String again.
func (s LoanState) String() string { return fmt.Sprintf("state %s", s) }

// Fixed converts to the underlying type first, so fmt sees a plain int.
type Fixed int

func (f Fixed) String() string { return fmt.Sprintf("fixed %d", int(f)) }

func main() {
	fmt.Println(Fixed(3))
	_ = LoanState(1) // calling LoanState.String would overflow the stack
}

Compiling and running that program is fine, but calling LoanState.String would exhaust the goroutine stack and crash with goroutine stack exceeds 1000000000-byte limit. go vet finds it:

e/recurse/main.go:8:64: fmt.Sprintf format %s with arg s causes recursive (example.com/shelf/e/recurse.LoanState).String method call

The cure is to convert to a type that has no String method (int(f), string(s)) before formatting, as Fixed does. The same applies to Error and Format.

Source: cmd/vet

Writing a Formatter

Formatter gives full control over every verb and flag. The fmt.State argument exposes the output stream (it is an io.Writer), Width(), Precision() and Flag(c), and fmt.FormatString(state, verb) (Go 1.20) reconstructs the original directive so that you can delegate to fmt for the underlying value. This type formats an amount of money stored as integer cents:

package main

import (
	"fmt"
	"strings"
)

type Money struct {
	Cents int64
	Cur   string
}

// Format implements fmt.Formatter. It honours width, the '-' and '+' flags
// and the verbs v and s, hands the integer verbs to the cents, and reports
// any other verb the way fmt does.
func (m Money) Format(f fmt.State, verb rune) {
	switch verb {
	case 'v', 's':
		sign := ""
		c := m.Cents
		if c < 0 {
			sign, c = "-", -c
		} else if f.Flag('+') {
			sign = "+"
		}
		out := fmt.Sprintf("%s%d.%02d %s", sign, c/100, c%100, m.Cur)
		if f.Flag('#') && verb == 'v' {
			out = fmt.Sprintf("Money{%d, %q}", m.Cents, m.Cur)
		}
		if w, ok := f.Width(); ok && len(out) < w {
			pad := strings.Repeat(" ", w-len(out))
			if f.Flag('-') {
				out += pad
			} else {
				out = pad + out
			}
		}
		fmt.Fprint(f, out)
	case 'd', 'x', 'X', 'o', 'b':
		// Reconstruct the directive (flags, width, precision, verb) and apply it to the cents.
		fmt.Fprintf(f, fmt.FormatString(f, verb), m.Cents)
	default:
		fmt.Fprintf(f, "%%!%c(Money=%d)", verb, m.Cents)
	}
}

func main() {
	m := Money{1999, "EUR"}
	fmt.Printf("[%v] [%+v] [%14v] [%-14s] [%d] [%#v] [%x] [%08d] [%q]\n", m, m, m, m, m, m, m, m, m)
	fmt.Println(Money{-250, "EUR"})
}
[19.99 EUR] [+19.99 EUR] [     19.99 EUR] [19.99 EUR     ] [1999] [Money{1999, "EUR"}] [7cf] [00001999] [%!q(Money=1999)]
-2.50 EUR

Implementing Formatter is rarely necessary; Stringer covers nearly every case. Reach for Formatter when width, alignment or %d-style verbs must behave consistently for a type, which is why the standard library uses it for types such as math/big numbers.

Errorf and wrapping with %w

fmt.Errorf formats like Sprintf and returns an error. If the format string contains the verb %w with an operand that implements error, the returned error wraps that operand: errors.Unwrap returns it, and errors.Is/errors.As look through it. Go 1.20 allows several %w verbs, in which case the result has an Unwrap() []error method.

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

var ErrInvalidISBN = errors.New("invalid ISBN")

func main() {
	_, err := os.Open("missing.json")
	wrapped := fmt.Errorf("load catalogue: %w", err)
	fmt.Println(wrapped)
	fmt.Println(errors.Is(wrapped, fs.ErrNotExist), errors.Unwrap(wrapped) == err)

	// %v formats the text but does not wrap: errors.Is cannot see through it.
	flat := fmt.Errorf("load catalogue: %v", err)
	fmt.Println(errors.Is(flat, fs.ErrNotExist), errors.Unwrap(flat))

	// Several %w verbs (Go 1.20) produce an error with Unwrap() []error.
	both := fmt.Errorf("import %q: %w; also %w", "bad.csv", ErrInvalidISBN, fs.ErrPermission)
	fmt.Println(both)
	fmt.Println(errors.Is(both, ErrInvalidISBN), errors.Is(both, fs.ErrPermission))
	if u, ok := both.(interface{ Unwrap() []error }); ok {
		fmt.Println("wrapped errors:", len(u.Unwrap()))
	}
}
load catalogue: open missing.json: no such file or directory
true true
false <nil>
import "bad.csv": invalid ISBN; also permission denied
true true
wrapped errors: 2

Use %w when callers should be able to match the cause and %v when the cause is an implementation detail you do not want to commit to as API. A %w whose operand is not an error is not an error in the compiler but go vet reports fmt.Errorf format %w has arg "text" of wrong type string, and at run time the result contains %!w(string=text). The whole error model, sentinel and typed errors, errors.AsType and errors.Join is in Error Handling.

Scanning

The scanning functions mirror the printing ones: Scan, Scanf and Scanln read from standard input, Sscan, Sscanf and Sscanln read from a string, and Fscan, Fscanf and Fscanln read from an io.Reader. Their operands are pointers to the variables to fill. The three flavours differ in how they treat newlines:

  • Scan treats newlines as ordinary white space, so operands may span lines.

  • Scanln stops at a newline, and after the last operand there must be a newline or end of input.

  • Scanf matches the format string: newlines in the input must match newlines in the format.

Each returns the number of operands successfully filled and an error; compare the count, not only the error, because n tells you how far the input got before failing. The input verbs are a subset of the printing verbs: %d, %x, %o, %b, %s, %q, %c, %v, %t, %f, %e, %g and %U, plus widths (%4d) that limit how many runes are consumed. A width limits the number of runes read; there is no precision, and the flags # and + are not implemented.

package main

import (
	"fmt"
	"strings"
)

type Loan struct {
	ISBN   string
	Member string
	Days   int
}

func main() {
	// Sscan treats newlines as space and fills one operand per space-separated token.
	var title string
	var pages int
	var price float64
	n, err := fmt.Sscan("Dune 412 9.99", &title, &pages, &price)
	fmt.Println(n, err, title, pages, price)

	// Sscanf matches the format exactly.
	var l Loan
	n, err = fmt.Sscanf("loan 9780441172719 to ana for 14 days", "loan %s to %s for %d days", &l.ISBN, &l.Member, &l.Days)
	fmt.Printf("%d %v %+v\n", n, err, l)

	// Errors report how many operands were filled before the failure.
	n, err = fmt.Sscanf("loan 9780441172719 to ana for many days", "loan %s to %s for %d days", &l.ISBN, &l.Member, &l.Days)
	fmt.Println(n, err)

	n, err = fmt.Sscan("7", &pages, &price)
	fmt.Println(n, err)

	// Sscanln stops at the newline and requires one to follow the last operand.
	var a, b int
	n, err = fmt.Sscanln("1 2\n3", &a, &b)
	fmt.Println(n, err, a, b)
	n, err = fmt.Sscanln("1\n2", &a, &b)
	fmt.Println(n, err)

	// Fscan reads from any io.Reader, but reads one rune at a time unless the
	// reader implements io.RuneScanner: wrap files in bufio.Reader.
	r := strings.NewReader("3\nDune 5\nSolaris 2\nNeuromancer 9\n")
	var count int
	fmt.Fscan(r, &count)
	for range count {
		var t string
		var copies int
		if _, err := fmt.Fscan(r, &t, &copies); err != nil {
			fmt.Println("scan:", err)
			break
		}
		fmt.Printf("%-12s %d\n", t, copies)
	}

	// Verbs for scanning: %d %x %q %c %v %s and a width limit.
	var year, month, day int
	fmt.Sscanf("20261018", "%4d%2d%2d", &year, &month, &day)
	fmt.Println(year, month, day)
	var hex int
	var q string
	var c rune
	fmt.Sscanf(`ff "two words" é`, "%x %q %c", &hex, &q, &c)
	fmt.Println(hex, q, string(c))
}
3 <nil> Dune 412 9.99
3 <nil> {ISBN:9780441172719 Member:ana Days:14}
2 expected integer
1 EOF
2 <nil> 1 2
1 unexpected newline
Dune         5
Solaris      2
Neuromancer  9
2026 10 18
255 two words é

Source: fmt: Scanning

Notes on that output:

  • Sscanf with a mismatching input reports expected integer and the count of operands already assigned (two), which is why the Loan value is left partly updated: the already-parsed operands are not rolled back.

  • Sscan("7", &pages, &price) filled one operand and then hit the end of the input. Check both n and err.

  • Sscanln("1\n2", &a, &b) fails with unexpected newline: Scanln does not look past the end of the line.

  • Fscan on a reader that does not implement io.RuneScanner (it needs UnreadRune) may lose one character of look-ahead between calls. Wrap files and network connections in a bufio.Reader before scanning from them, and use one reader for the whole stream, not a new one per call.

  • A type can control how it is scanned by implementing fmt.Scanner (Scan(state fmt.ScanState, verb rune) error). This is rare; encoding.TextUnmarshaler is the usual hook for new types.

fmt.Scan and friends are fine for exercises, small tools and parsing a short, rigid line such as a log record. They are slow for large inputs, tolerant in surprising ways (any amount of white space between tokens) and cannot report where an error happened. For lines of input use bufio.Scanner (below) plus strings.Cut/strings.Fields and strconv; for structured data use a real format and the matching package (encoding/json, encoding/csv).

The strings package

A Go string is an immutable sequence of bytes, and the strings package is the toolbox for that type; it never modifies its arguments and returns new strings that may share memory with the input. Functions are byte- and UTF-8-aware as documented: Index returns a byte offset, Count("five", "") is the number of runes plus one, and case mapping works on runes. The sections below group the whole API; the tables name each function so you can find it, and the examples show the behaviour that is easy to get wrong.

Cut, CutPrefix, CutSuffix and CutLast

Cut(s, sep) (Go 1.18) slices a string around the first occurrence of sep and reports whether it was found. It replaces most uses of Index followed by slicing, and of SplitN(s, sep, 2). CutPrefix and CutSuffix (Go 1.20) do the same for an affix, and unlike TrimPrefix they tell you whether the affix was present. CutLast(s, sep), new in Go 1.27, splits around the last occurrence, which is what you want for file extensions, host:port pairs and the final path element. bytes.CutLast is its counterpart for []byte.

package main

import (
	"fmt"
	"strings"
)

func main() {
	// Cut splits around the first occurrence of a separator.
	key, value, found := strings.Cut("isbn=9780441172719", "=")
	fmt.Printf("%q %q %v\n", key, value, found)
	before, after, found := strings.Cut("no separator here", "=")
	fmt.Printf("%q %q %v\n", before, after, found)

	// CutLast (Go 1.27) splits around the last occurrence.
	dir, file, ok := strings.CutLast("data/shelf/books.json", "/")
	fmt.Printf("%q %q %v\n", dir, file, ok)
	name, ext, ok := strings.CutLast("archive.tar.gz", ".")
	fmt.Printf("%q %q %v\n", name, ext, ok)
	// The first-occurrence version gives a different answer.
	name, ext, _ = strings.Cut("archive.tar.gz", ".")
	fmt.Printf("%q %q\n", name, ext)

	// CutPrefix and CutSuffix report whether the affix was present.
	rest, ok := strings.CutPrefix("Bearer fake-token-123", "Bearer ")
	fmt.Printf("%q %v\n", rest, ok)
	rest, ok = strings.CutPrefix("Basic fake", "Bearer ")
	fmt.Printf("%q %v\n", rest, ok)
	base, ok := strings.CutSuffix("report.csv", ".csv")
	fmt.Printf("%q %v\n", base, ok)

	// TrimPrefix cannot tell you whether it trimmed anything, and Trim takes a cutset.
	fmt.Println(strings.TrimPrefix("xxfoo", "x"), strings.Trim("xxfooxx", "x"), strings.TrimLeft("xyxfoo", "xy"))
	fmt.Println(strings.TrimSpace("\t shelf \n"), strings.TrimFunc("123abc456", func(r rune) bool { return r >= '0' && r <= '9' }))
}
"isbn" "9780441172719" true
"no separator here" "" false
"data/shelf" "books.json" true
"archive.tar" "gz" true
"archive" "tar.gz"
"fake-token-123" true
"Basic fake" false
"report" true
xfoo foo foo
shelf abc

CutLast appears in the Go 1.27 API listing, and it is not available in Go 1.26. A module whose go.mod says go 1.26 still builds with the Go 1.27 toolchain, but go vet (through its stdversion analyzer) reports the use of a newer API than the module declares:

main.go:9:27: strings.CutLast requires go1.27 or later (module is go1.26)

Until the oldest Go release you support is 1.27, write i := strings.LastIndex(s, sep) and slice by hand, or use strings.CutSuffix/path.Split/filepath.Ext when the task is really about paths. See Toolchain Versions and Compatibility for how the go line and the toolchain interact.

Two pitfalls: Cut returns three values, so key := strings.Cut(...) is a compile error (see the end of this page), and TrimLeft/TrimRight/Trim take a set of characters, not a prefix: TrimLeft("xyxfoo", "xy") removes every leading x and y, whereas TrimPrefix removes one exact prefix once.

Splitting, fields and joining

Split(s, sep) splits around every non-overlapping sep and returns all pieces including empty ones, SplitN limits the number of pieces, SplitAfter keeps the separator at the end of each piece, and an empty sep splits into UTF-8 sequences. Fields splits around runs of white space (as defined by unicode.IsSpace) and never returns empty strings, which is almost always what you want for human-written input; FieldsFunc uses your own predicate. Join is the inverse of Split, and Repeat and Map complete the group (Map drops a rune when the function returns a negative value).

package main

import (
	"fmt"
	"strings"
)

func main() {
	csv := "Dune,Herbert,,1965"
	fmt.Printf("%q\n", strings.Split(csv, ","))
	fmt.Printf("%q\n", strings.SplitN(csv, ",", 2))
	fmt.Printf("%q\n", strings.SplitAfter(csv, ","))
	fmt.Printf("%q\n", strings.Split("abc", ""))
	fmt.Printf("%q\n", strings.Split("", ","))

	// Fields splits around runs of white space and never returns empty strings.
	ws := "  Dune \t Solaris\n\nNeuromancer  "
	fmt.Printf("%q\n", strings.Fields(ws))
	fmt.Printf("%q\n", strings.Split(ws, " "))
	fmt.Printf("%q\n", strings.FieldsFunc("a1b22c333", func(r rune) bool { return r >= '0' && r <= '9' }))

	// Join is the inverse of Split; Repeat and Map round off the basics.
	fmt.Println(strings.Join([]string{"a", "b", "c"}, " > "))
	fmt.Println(strings.Repeat("=-", 5))
	fmt.Println(strings.Map(func(r rune) rune {
		if r == '-' {
			return -1 // drop the rune
		}
		return r
	}, "978-0-441-17271-9"))

	// Searching.
	s := "the shelf holds the books"
	fmt.Println(strings.Contains(s, "shelf"), strings.ContainsAny(s, "xyz"), strings.ContainsRune(s, 'h'))
	fmt.Println(strings.Index(s, "the"), strings.LastIndex(s, "the"), strings.IndexByte(s, 'h'), strings.IndexAny(s, "ob"), strings.Index(s, "zzz"))
	fmt.Println(strings.Count(s, "the"), strings.Count("five", ""), strings.HasPrefix(s, "the"), strings.HasSuffix(s, "books"))
	fmt.Println(strings.EqualFold("Straße", "STRASSE"), strings.EqualFold("Go", "GO"), strings.Compare("a", "b"))
	fmt.Println(strings.ToUpper("shelf é"), strings.ToLower("SHELF É"), strings.ToTitle("shelf"))
	fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2), strings.ReplaceAll("oink oink", "oink", "moo"))
}
["Dune" "Herbert" "" "1965"]
["Dune" "Herbert,,1965"]
["Dune," "Herbert," "," "1965"]
["a" "b" "c"]
[""]
["Dune" "Solaris" "Neuromancer"]
["" "" "Dune" "\t" "Solaris\n\nNeuromancer" "" ""]
["a" "b" "c"]
a > b > c
=-=-=-=-=-
9780441172719
true false true
0 16 1 11 -1
2 5 true true
false true -1
SHELF É shelf é SHELF
oinky oinky oink moo moo

Reading the output:

  • Split("", ",") returns a slice with one empty string, not an empty slice, so len(Split(s, ",")) is never zero. When the input may be empty, check it first.

  • Splitting " Dune \t Solaris..." around " " leaves empty strings and glued tokens; Fields is the right tool.

  • EqualFold uses simple Unicode case folding: it knows "Go" equals "GO", but not that "ß" expands to "SS". Locale-correct comparison needs golang.org/x/text.

  • Count(s, "") returns one more than the number of runes.

  • Compare exists for symmetry with bytes.Compare; the comparison operators < and == on strings do the same job and are usually clearer. For sorting use slices.Sort or cmp.Compare.

Searching, prefixes and case

Functions Purpose

Contains, ContainsAny, ContainsRune, ContainsFunc

Report whether the string holds a substring, any rune from a set, one rune, or a rune satisfying a predicate.

HasPrefix, HasSuffix

Test an affix; use CutPrefix/CutSuffix when you also need the rest.

Index, IndexByte, IndexRune, IndexAny, IndexFunc, LastIndex and friends

Byte offset of the first (or last) match, -1 when absent.

Count

Non-overlapping occurrences.

ToUpper, ToLower, ToTitle, ToUpperSpecial, ToLowerSpecial

Rune-by-rune case mapping; the Special forms take a unicode.SpecialCase such as unicode.TurkishCase.

EqualFold

Case-insensitive equality with simple folding.

Title

Deprecated since Go 1.18: its word-boundary rule does not handle Unicode punctuation. Use golang.org/x/text/cases for proper title casing.

Trim, TrimSpace, TrimFunc, TrimLeft, TrimRight, TrimPrefix, TrimSuffix

Remove characters from the ends; the cutset forms take a set, the Prefix/Suffix forms an exact string.

Replace, ReplaceAll, NewReplacer

Substitution of one or many substrings.

Clone

Copy a string so that a small substring does not keep a huge backing array alive (Go 1.18).

ToValidUTF8

Replace invalid UTF-8 sequences with a replacement string (Go 1.13).

Building strings: Builder, Reader and Replacer

Because strings are immutable, s += x in a loop copies the accumulated text on every iteration and is quadratic in the total size. strings.Builder (Go 1.10) holds a growable byte slice, never copies it when you call String(), and implements io.Writer so it works with fmt.Fprintf. Call Grow(n) first when you can estimate the final size. A Builder must not be copied after first use (it panics detecting that), and its zero value is ready to use.

package main

import (
	"fmt"
	"strings"
	"testing"
)

var titles = []string{"Dune", "Solaris", "Neuromancer", "Hyperion", "Foundation", "Ubik", "Kindred", "Dawn"}

func concat(n int) string {
	s := ""
	for i := range n {
		s += titles[i%len(titles)] + ", "
	}
	return s
}

func builder(n int) string {
	var sb strings.Builder
	for i := range n {
		sb.WriteString(titles[i%len(titles)])
		sb.WriteString(", ")
	}
	return sb.String()
}

func presized(n int) string {
	var sb strings.Builder
	sb.Grow(n * 14) // a good estimate avoids every reallocation
	for i := range n {
		sb.WriteString(titles[i%len(titles)])
		sb.WriteString(", ")
	}
	return sb.String()
}

func main() {
	const n = 1000
	for _, f := range []struct {
		name string
		fn   func(int) string
	}{{"s += ...", concat}, {"Builder", builder}, {"Builder+Grow", presized}} {
		allocs := testing.AllocsPerRun(20, func() { _ = f.fn(n) })
		fmt.Printf("%-13s %7.0f allocations for %d appends\n", f.name, allocs, n)
	}

	var sb strings.Builder
	sb.WriteByte('[')
	sb.WriteRune('é')
	fmt.Fprintf(&sb, "%03d", 7)
	sb.WriteByte(']')
	fmt.Println(sb.String(), sb.Len())
	sb.Reset()
	fmt.Println(sb.Len())

	// strings.Join does the sizing for you when you already have a slice.
	fmt.Println(strings.Join(titles[:3], ", "))
}
s += ...         1000 allocations for 1000 appends
Builder            15 allocations for 1000 appends
Builder+Grow        1 allocations for 1000 appends
[é007] 7
0
Dune, Solaris, Neuromancer

Allocation counts, not wall-clock times, are the stable thing to measure: one allocation per append for concatenation, a logarithmic number for a Builder that grows by doubling, and one for a presized Builder. Prefer strings.Join when you already hold the pieces in a slice, because it computes the final size itself.

strings.Reader (from strings.NewReader) turns a string into an io.Reader, io.ByteScanner, io.RuneScanner, io.Seeker, io.ReaderAt and io.WriterTo without copying it; it is the standard way to feed a string to an API that wants a reader, including fmt.Fscan, bufio.Scanner and json.NewDecoder.

strings.Replacer performs many replacements in a single pass. The replacements are applied in the order of the arguments for matches that start at the same position, and the output is never rescanned, which is exactly what makes a Replacer correct for escaping and a chain of ReplaceAll calls wrong:

package main

import (
	"fmt"
	"html"
	"os"
	"strings"
	"unicode/utf8"
)

func main() {
	// A Replacer makes all replacements in a single pass over the input.
	r := strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
	fmt.Println(r.Replace("if a < b && c > d"))
	fmt.Println(html.EscapeString("if a < b && c > d"))

	// Replacements do not rescan their own output, unlike chained ReplaceAll calls.
	swap := strings.NewReplacer("a", "b", "b", "a")
	fmt.Println(swap.Replace("abba"), strings.ReplaceAll(strings.ReplaceAll("abba", "a", "b"), "b", "a"))

	// WriteString writes straight to a Writer.
	r.WriteString(os.Stdout, "<done>\n")

	// Clone detaches a substring from a large backing string.
	big := strings.Repeat("x", 1<<20) + "needle"
	small := big[len(big)-6:]
	owned := strings.Clone(small)
	fmt.Println(small == owned, len(owned), utf8.RuneCountInString(owned))
}
if a &lt; b &amp;&amp; c &gt; d
if a &lt; b &amp;&amp; c &gt; d
baab aaaa
&lt;done&gt;
true 6 6

In real programs prefer the dedicated escaping function (html.EscapeString, url.QueryEscape, strconv.Quote, template.HTMLEscapeString) over a hand-made Replacer; the example shows the single-pass property, not a recommendation to write your own escaper. A Replacer is safe for concurrent use after construction, so build it once at package level.

Iterator functions (Go 1.24)

Go 1.24 added iterator-returning variants of the functions that build slices: Lines, SplitSeq, SplitAfterSeq, FieldsSeq and FieldsFuncSeq. Each returns an iter.Seq[string] that you range over with for ... range (see Iterators and Range-over-Func). They produce each piece on demand and allocate no slice, so scanning a large string, or stopping at the first match, costs nothing for the parts you never reach. The same functions exist in bytes, yielding []byte sub-slices that alias the original.

package main

import (
	"fmt"
	"slices"
	"strings"
)

func main() {
	csv := "Dune,Herbert,1965"
	for part := range strings.SplitSeq(csv, ",") {
		fmt.Printf("%q ", part)
	}
	fmt.Println()

	// Lines yields each line including its terminator.
	text := "first\nsecond\r\nthird"
	for line := range strings.Lines(text) {
		fmt.Printf("%q ", line)
	}
	fmt.Println()

	for f := range strings.FieldsSeq("  Dune  Solaris \t Ubik ") {
		fmt.Print("<", f, "> ")
	}
	fmt.Println()

	for part := range strings.SplitAfterSeq("a.b.c", ".") {
		fmt.Printf("%q ", part)
	}
	fmt.Println()

	for f := range strings.FieldsFuncSeq("a1b22c", func(r rune) bool { return r >= '0' && r <= '9' }) {
		fmt.Printf("%q ", f)
	}
	fmt.Println()

	// Iterators compose with the slices package and stop early without building a slice.
	words := slices.Collect(strings.FieldsSeq("one two three four"))
	fmt.Println(words, len(words))
	for w := range strings.FieldsSeq("one two three four") {
		if w == "two" {
			fmt.Println("stopped at", w)
			break
		}
	}
	longest := slices.MaxFunc(words, func(a, b string) int { return len(a) - len(b) })
	fmt.Println(longest)
}
"Dune" "Herbert" "1965"
"first\n" "second\r\n" "third"
<Dune> <Solaris> <Ubik>
"a." "b." "c"
"a" "b" "c"
[one two three four] 4
stopped at two
three

Lines keeps the line terminator ("second\r\n"), unlike bufio.Scanner, and a final line without a terminator is yielded as is. Combine an iterator with slices.Collect when you do need a slice, as FieldsSeq does above.

The bytes package

bytes has the same API as strings for []byte, so everything above applies with []byte operands: Cut, CutPrefix, CutSuffix, CutLast (1.27), Fields, Split, Contains, Index, Trim*, Equal, Compare, ToUpper, Replace, Map, the *Seq iterators (1.24) and Lines. Two differences: results alias the input where they can (a sub-slice of the argument, not a copy), and use bytes.Equal(a, b) rather than ==, which is not defined for slices. string(a) == string(b) also works without allocation when it appears directly in a comparison.

The extra type is bytes.Buffer, a growable buffer of bytes that is both a Reader and a Writer, with methods such as Write, WriteString, WriteByte, WriteRune, ReadString, ReadBytes, ReadByte, UnreadByte, Next, Bytes, String, Len, Reset, Truncate, WriteTo and ReadFrom. Peek(n) (Go 1.26) returns the next n bytes without consuming them. The zero value is ready to use, and Bytes() returns a slice that aliases the buffer, valid only until the next modification.

package main

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

func main() {
	// bytes mirrors strings for []byte values.
	line := []byte("title=Dune;pages=412")
	k, v, ok := bytes.Cut(line, []byte("="))
	fmt.Printf("%s %s %v\n", k, v, ok)
	dir, file, ok := bytes.CutLast([]byte("a/b/c.txt"), []byte("/"))
	fmt.Printf("%s %s %v\n", dir, file, ok)
	fmt.Printf("%q\n", bytes.Fields([]byte(" a  b ")))
	fmt.Println(bytes.Equal([]byte("x"), []byte("x")), bytes.Compare([]byte("a"), []byte("b")), bytes.HasPrefix(line, []byte("title")))

	// Buffer is a growable byte buffer that is also a Reader and a Writer.
	var buf bytes.Buffer // the zero value is ready to use
	buf.WriteString("Dune\n")
	buf.Write([]byte("Solaris\n"))
	fmt.Fprintf(&buf, "%s\n", "Ubik")
	fmt.Println(buf.Len())

	first, err := buf.ReadString('\n')
	fmt.Printf("%q %v\n", first, err)
	b, _ := buf.ReadByte()
	fmt.Printf("%c\n", b)
	buf.UnreadByte()

	p, _ := buf.Peek(4) // Go 1.26: look ahead without consuming
	fmt.Printf("%q\n", p)

	rest, _ := io.ReadAll(&buf)
	fmt.Printf("%q %d\n", rest, buf.Len())

	// WriteTo drains a buffer into a Writer.
	buf.WriteString("copied to stdout\n")
	buf.WriteTo(os.Stdout)

	// bytes.NewReader and strings.NewReader give a Reader over existing data
	// without copying; they also implement Seeker, ReaderAt and WriterTo.
	r := bytes.NewReader([]byte("0123456789"))
	r.Seek(5, io.SeekStart)
	tail, _ := io.ReadAll(r)
	fmt.Printf("%s %d\n", tail, r.Size())
}
title Dune;pages=412 true
a/b c.txt true
["a" "b"]
true -1 true
18
"Dune\n" <nil>
S
"Sola"
"Solaris\nUbik\n" 0
copied to stdout
56789 10

Use bytes.Buffer when you need to read back what you wrote (assembling a request body, capturing output in a test) and strings.Builder when you only need the final string. The conversion of a strings.Builder to a string is free, while Buffer.String() copies. bytes.NewReader is the []byte analogue of strings.NewReader. Everything about io.Reader, io.Writer and composing them is in I/O, Files and the Filesystem.

strconv: numbers, booleans and quoting

strconv converts basic types to and from strings. The first rule is that conversion between string and numbers is never done with a plain type conversion: string(65) yields "A" (and go vet warns with stringintconv), whereas strconv.Itoa(65) yields "65". The parsing functions return an error that is a *strconv.NumError; it records the function, the input and the cause, and its Unwrap method (Go 1.14) lets errors.Is match strconv.ErrSyntax (not a valid number) and strconv.ErrRange (valid but does not fit the type).

Functions Purpose

Atoi, Itoa

Base-10 int conversions; Atoi accepts an optional sign and nothing else (no spaces, no underscores).

ParseInt, ParseUint

(s, base, bitSize): base 2 to 36, or 0 to infer from a 0x, 0o or 0b prefix and allow _ separators; bitSize 0, 8, 16, 32 or 64 selects the range the value must fit in.

ParseFloat

(s, bitSize) with bitSize 32 or 64; accepts inf, infinity and nan in any case, hexadecimal floats and underscores only with a base prefix.

ParseBool, FormatBool

Accept 1, t, T, TRUE, true, True and the false counterparts; anything else is a syntax error.

ParseComplex

Complex numbers such as 1+2i.

FormatInt, FormatUint, FormatFloat

FormatInt(n, base); FormatFloat(f, fmt, prec, bitSize) with format f, e, g, b, x and precision -1 for the shortest round-tripping form.

AppendInt, AppendUint, AppendFloat, AppendBool, AppendQuote and friends

Append the text to a []byte without creating a string.

Quote, QuoteToASCII, QuoteToGraphic, QuoteRune, Unquote, UnquoteChar, QuotedPrefix, CanBackquote

Go string-literal syntax in both directions.

package main

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

func main() {
	// Atoi and Itoa: base 10, type int.
	n, err := strconv.Atoi("412")
	fmt.Println(n, err)
	_, err = strconv.Atoi("41x")
	fmt.Println(err)
	_, err = strconv.Atoi("")
	fmt.Println(err)
	_, err = strconv.Atoi(" 7") // no trimming of white space
	fmt.Println(err)
	fmt.Println(strconv.Itoa(-42) + "!")

	// ParseInt takes a base (0 means "look at the prefix") and a bit size.
	v, err := strconv.ParseInt("0x1F", 0, 64)
	fmt.Println(v, err)
	v, err = strconv.ParseInt("1_000", 0, 64) // underscores only with base 0
	fmt.Println(v, err)
	v, err = strconv.ParseInt("-101", 2, 8)
	fmt.Println(v, err)
	v, err = strconv.ParseInt("300", 10, 8)
	fmt.Println(v, err) // out of range: the value is clamped
	u, err := strconv.ParseUint("-1", 10, 64)
	fmt.Println(u, err)

	// Telling the failure modes apart.
	_, err = strconv.ParseInt("999999999999999999999", 10, 64)
	var ne *strconv.NumError
	if errors.As(err, &ne) {
		fmt.Printf("func=%s num=%q err=%v range=%v syntax=%v\n", ne.Func, ne.Num, ne.Err,
			errors.Is(err, strconv.ErrRange), errors.Is(err, strconv.ErrSyntax))
	}

	// Floats.
	f, err := strconv.ParseFloat("3.14159", 64)
	fmt.Println(f, err)
	f, err = strconv.ParseFloat("1e400", 64)
	fmt.Println(f, err)
	f, _ = strconv.ParseFloat("NaN", 64)
	g, _ := strconv.ParseFloat("-inf", 64)
	fmt.Println(f, g)
	f32, _ := strconv.ParseFloat("0.1", 32)
	fmt.Println(f32, float32(f32))

	// Booleans accept 1, t, T, TRUE, true, True and the false spellings.
	for _, s := range []string{"1", "T", "true", "False", "yes"} {
		b, err := strconv.ParseBool(s)
		fmt.Print(b, " ", err, "; ")
	}
	fmt.Println()

	// Formatting.
	fmt.Println(strconv.FormatInt(255, 2), strconv.FormatInt(-255, 16), strconv.FormatUint(255, 36))
	fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64), strconv.FormatFloat(3.14159, 'e', -1, 64),
		strconv.FormatFloat(1234567.0, 'g', -1, 64), strconv.FormatFloat(0.1, 'f', -1, 32))
	fmt.Println(strconv.FormatBool(true))

	// Append* variants write into an existing buffer without allocating a string.
	buf := make([]byte, 0, 64)
	buf = append(buf, "pages="...)
	buf = strconv.AppendInt(buf, 412, 10)
	buf = append(buf, " title="...)
	buf = strconv.AppendQuote(buf, "Dune")
	fmt.Println(string(buf))
}
412 <nil>
strconv.Atoi: parsing "41x": invalid syntax
strconv.Atoi: parsing "": invalid syntax
strconv.Atoi: parsing " 7": invalid syntax
-42!
31 <nil>
1000 <nil>
-5 <nil>
127 strconv.ParseInt: parsing "300": value out of range
0 strconv.ParseUint: parsing "-1": invalid syntax
func=ParseInt num="999999999999999999999" err=value out of range range=true syntax=false
3.14159 <nil>
+Inf strconv.ParseFloat: parsing "1e400": value out of range
NaN -Inf
0.10000000149011612 0.1
true <nil>; true <nil>; true <nil>; false <nil>; false strconv.ParseBool: parsing "yes": invalid syntax;
11111111 -ff 73
3.14 3.14159e+00 1.234567e+06 0.1
true
pages=412 title="Dune"

Points to take from the output:

  • On a range error ParseInt returns the clamped value (127 for an 8-bit parse of 300) together with the error, so always check the error before using the value.

  • The error text names the function, quotes the input and gives the cause: strconv.Atoi: parsing "41x": invalid syntax. Wrapping it with fmt.Errorf("pages: %w", err) keeps both the context and the errors.Is behaviour.

  • Atoi does not trim white space; call strings.TrimSpace first when the input comes from a user.

  • ParseFloat("0.1", 32) returns a float64 holding the nearest float32; converting it with float32(f) is the correct way to get the 32-bit value. Floats are IEEE 754 approximations and are the wrong type for money.

  • Append* variants are what fmt itself uses internally; they are the fastest way to build numeric text into a reusable buffer.

Quoting

Quote returns a double-quoted Go string literal in which control characters, invalid UTF-8 and non-printable runes are escaped; QuoteToASCII also escapes every non-ASCII rune, and QuoteToGraphic keeps characters that Unicode classes as graphic. Unquote reverses it for interpreted strings, raw (backquoted) strings and rune literals. %q in fmt uses the same rules.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	s := "tab\there \"quoted\" é 日本 \x00 \xff"
	fmt.Println(strconv.Quote(s))
	fmt.Println(strconv.QuoteToASCII(s))
	fmt.Println(strconv.QuoteToGraphic("a\tb"))
	fmt.Println(strconv.QuoteRune('☺'), strconv.QuoteRuneToASCII('☺'))
	fmt.Println(strconv.CanBackquote("plain text"), strconv.CanBackquote("has `tick"))

	// Unquote accepts Go string literals: interpreted, raw and rune forms.
	for _, lit := range []string{`"a\tb"`, "`raw\\n`", `'x'`, `"unterminated`, `noquotes`} {
		u, err := strconv.Unquote(lit)
		fmt.Printf("%-16s -> %q %v\n", lit, u, err)
	}

	// QuotedPrefix (Go 1.17) returns the leading quoted string of its argument.
	p, err := strconv.QuotedPrefix(`"title" rest of the line`)
	fmt.Printf("%s %v\n", p, err)

	// %q uses the same rules as Quote.
	fmt.Printf("%q %+q %#q\n", "é\n", "é\n", "plain")
}
"tab\there \"quoted\" é 日本 \x00 \xff"
"tab\there \"quoted\" \u00e9 \u65e5\u672c \x00 \xff"
"a\tb"
'☺' '\u263a'
true false
"a\tb"           -> "a\tb" <nil>
`raw\n`          -> "raw\\n" <nil>
'x'              -> "x" <nil>
"unterminated    -> "" invalid syntax
noquotes         -> "" invalid syntax
"title" <nil>
"é\n" "\u00e9\n" `plain`

Use %q or strconv.Quote whenever you put untrusted or user-supplied text into an error message or a log line: it makes control characters, trailing spaces and invalid bytes visible and prevents a newline in the input from forging a second log record.

The unicode packages

unicode classifies and maps runes; unicode/utf8 (see Strings, Runes and UTF-8) encodes and decodes them. The predicates are IsLetter, IsDigit (decimal digits in any script), IsNumber (digits plus fractions, Roman numerals and so on), IsSpace, IsPunct, IsSymbol, IsMark, IsUpper, IsLower, IsTitle, IsControl, IsGraphic and IsPrint. The mappers are ToUpper, ToLower, ToTitle and SimpleFold. Scripts and general categories are *unicode.RangeTable values such as unicode.Han, unicode.Greek and unicode.Lu, tested with unicode.Is and unicode.In. The constant unicode.Version is "17.0.0" in Go 1.27 (the tables moved from Unicode 15 to 17 in Go 1.27; Go 1.25 had added the Cn and LC categories and unicode.CategoryAliases).

package main

import (
	"fmt"
	"strings"
	"unicode"
	"unicode/utf8"
)

func mark(b bool) rune {
	if b {
		return 'x'
	}
	return '.'
}

func main() {
	fmt.Println("rune  L N P S W M")
	for _, r := range []rune{'A', 'é', '7', '٣', ' ', '_', '日', '€', '😀', 0x0301} {
		fmt.Printf("%-5q %c %c %c %c %c %c\n", r,
			mark(unicode.IsLetter(r)), mark(unicode.IsNumber(r)), mark(unicode.IsPunct(r)),
			mark(unicode.IsSymbol(r)), mark(unicode.IsSpace(r)), mark(unicode.IsMark(r)))
	}

	fmt.Println("unicode version:", unicode.Version)

	// Scripts and categories are RangeTables.
	for _, r := range "Go日本ひらがなカタカナ한" {
		switch {
		case unicode.Is(unicode.Han, r):
			fmt.Print("Han ")
		case unicode.Is(unicode.Hiragana, r):
			fmt.Print("Hiragana ")
		case unicode.Is(unicode.Katakana, r):
			fmt.Print("Katakana ")
		case unicode.In(r, unicode.Latin):
			fmt.Print("Latin ")
		default:
			fmt.Print("other ")
		}
	}
	fmt.Println()

	// Case mapping is rune-by-rune; it has special cases for a few scripts.
	fmt.Println(string(unicode.ToUpper('é')), string(unicode.ToLower('Σ')), string(unicode.ToTitle('dž')))
	fmt.Println(strings.ToUpper("straße"), strings.ToUpperSpecial(unicode.TurkishCase, "istanbul"))

	// Strip everything that is not a letter or digit.
	clean := strings.Map(func(r rune) rune {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			return r
		}
		return -1
	}, "Dune: (1965) – Herbert!")
	fmt.Println(clean)

	// Counting runes versus bytes versus user-perceived characters.
	s := "é" // e + combining acute accent
	fmt.Println(len(s), utf8.RuneCountInString(s), s)
}
rune  L N P S W M
'A'   x . . . . .
'é'   x . . . . .
'7'   . x . . . .
'٣'   . x . . . .
' '   . . . . x .
'_'   . . x . . .
'日'   x . . . . .
'€'   . . . x . .
'😀'   . . . x . .
'́'   . . . . . x
unicode version: 17.0.0
Latin Latin Han Han Hiragana Hiragana Hiragana Hiragana Katakana Katakana Katakana Katakana other
É σ Dž
STRAßE İSTANBUL
Dune1965Herbert
3 2 é

The columns in the first table are IsLetter, IsNumber, IsPunct, IsSymbol, IsSpace and IsMark. Remarks:

  • IsLetter('日') is true, so identifier-like checks must decide whether non-ASCII letters are acceptable. Go itself accepts them in identifiers.

  • IsDigit('٣') (an Arabic-Indic digit) is true while strconv.Atoi("٣") fails: classification and parsing are different questions. To accept only ASCII digits write r >= '0' && r <= '9'.

  • unicode.ToUpper('é') is 'É' but strings.ToUpper("straße") stays "STRAßE", because Go does simple one-to-one case mapping and does not apply the one-to-many mapping that turns ß into SS. Language-sensitive case conversion (Turkish dotless i, Greek final sigma) needs unicode.SpecialCase or golang.org/x/text/cases.

  • A "character" as a user sees it may be several runes (e plus a combining accent is two runes and three bytes here), so neither len nor utf8.RuneCountInString counts characters. Normalization and grapheme-cluster segmentation are in golang.org/x/text/unicode/norm and are discussed in Strings, Runes and UTF-8.

regexp

The regexp package implements regular expressions with the RE2 syntax: Perl-like operators, Unicode classes and non-capturing groups, but deliberately without backreferences (\1) and lookaround ((?=...), (?<=...)). The trade is a guarantee: matching takes time linear in the size of the input, regardless of the pattern, and a *Regexp is safe for concurrent use by several goroutines. A backtracking engine such as those of Perl, Java or Python can take exponential time on a hostile pattern and input (the "regular expression denial of service" problem); Go’s engine cannot, so it is safe for matching untrusted input, though a huge pattern still costs memory and compile time and user-supplied patterns should still be size-limited.

Compile, match, find and replace

regexp.Compile returns (*Regexp, error); regexp.MustCompile panics on a bad pattern and is for constants, normally in a package-level var so that the pattern is compiled once, at start-up, rather than inside a loop. MatchString tells whether any part of the text matches, so anchor with ^ and $ when the whole string must match. Raw string literals (backquotes) avoid doubling every backslash.

The method names follow a regular scheme: the function is Find, FindAll, FindSubmatch or FindAllSubmatch; add String for a string input (otherwise []byte); add Index to get byte offsets instead of text. All variants take a limit n (negative for no limit). A match that is empty and adjacent to the previous match is skipped. Replace* substitutes: ReplaceAllString expands $1 and ${name} in the replacement, ReplaceAllLiteralString does not, and ReplaceAllStringFunc calls a function with each match.

package main

import (
	"fmt"
	"regexp"
)

// Compile once, at package level; a *Regexp is safe for concurrent use.
var isbnRE = regexp.MustCompile(`^(?:97[89])-?\d{1,5}-?\d+-?\d+-?\d$`)

var loanRE = regexp.MustCompile(`(?P<isbn>\d{13}) lent to (?P<member>\w+) on (?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)`)

func main() {
	fmt.Println(isbnRE.MatchString("978-0-441-17271-9"), isbnRE.MatchString("12345"))

	const log = "9780441172719 lent to ana on 2026-10-18; 9780553283686 lent to bob on 2026-10-19"

	// Find* functions: String/[]byte, All, Index, Submatch variants.
	fmt.Println(loanRE.FindString(log))
	fmt.Println(loanRE.FindStringIndex(log))
	fmt.Printf("%q\n", loanRE.FindStringSubmatch(log))
	fmt.Println(loanRE.FindAllStringSubmatchIndex(log, -1))
	for _, m := range loanRE.FindAllStringSubmatch(log, -1) {
		fmt.Println(m[loanRE.SubexpIndex("member")], m[loanRE.SubexpIndex("year")])
	}
	fmt.Println(loanRE.NumSubexp(), loanRE.SubexpNames())

	// Replace*: $1, ${name} expand submatches; a literal $ needs $$; ReplaceAllLiteral does no expansion.
	fmt.Println(loanRE.ReplaceAllString(log, "${member}: ${day}/${month}/${year}"))
	fmt.Println(loanRE.ReplaceAllString(log, "$member_x"), "<- $member_x is the (empty) group named member_x")
	fmt.Println(loanRE.ReplaceAllLiteralString(log, "[$1]"))
	fmt.Println(loanRE.ReplaceAllStringFunc(log, func(m string) string { return fmt.Sprint(len(m), " bytes") }))

	// Expand builds output from a template and one match.
	var out []byte
	m := loanRE.FindStringSubmatchIndex(log)
	out = loanRE.ExpandString(out, "$member borrowed $isbn\n", log, m)
	fmt.Print(string(out))

	// Split with a regular expression.
	sep := regexp.MustCompile(`\s*[,;]\s*`)
	fmt.Printf("%q\n", sep.Split("a , b;c ;  d", -1))

	// Flags: (?i) case-insensitive, (?s) dot matches newline, (?m) multi-line, (?U) ungreedy.
	fmt.Println(regexp.MustCompile(`(?i)dune`).FindString("DUNE and dune"))
	fmt.Printf("%q %q\n", regexp.MustCompile(`a.+b`).FindString("aXbXb"), regexp.MustCompile(`a.+?b`).FindString("aXbXb"))
	fmt.Printf("%q\n", regexp.MustCompile(`(?m)^\w+`).FindAllString("one two\nthree four", -1))

	// QuoteMeta escapes user input so it can be embedded in a pattern.
	user := "a.b*c"
	fmt.Println(regexp.QuoteMeta(user), regexp.MustCompile(regexp.QuoteMeta(user)).MatchString("aXbbc"))

	// Compile returns an error instead of panicking.
	_, err := regexp.Compile(`(unclosed`)
	fmt.Println(err)
	_, err = regexp.Compile(`(?<=a)b`)
	fmt.Println(err)
	_, err = regexp.Compile(`(a)\1`)
	fmt.Println(err)
	prefix, complete := regexp.MustCompile(`shelf-\d+`).LiteralPrefix()
	fmt.Println(isbnRE.String(), prefix, complete)
}
true false
9780441172719 lent to ana on 2026-10-18
[0 39]
["9780441172719 lent to ana on 2026-10-18" "9780441172719" "ana" "2026" "10" "18"]
[[0 39 0 13 22 25 29 33 34 36 37 39] [41 80 41 54 63 66 70 74 75 77 78 80]]
ana 2026
bob 2026
5 [ isbn member year month day]
ana: 18/10/2026; bob: 19/10/2026
;  <- $member_x is the (empty) group named member_x
[$1]; [$1]
39 bytes; 39 bytes
ana borrowed 9780441172719
["a" "b" "c" "d"]
DUNE
"aXbXb" "aXb"
["one" "three"]
a\.b\*c false
error parsing regexp: missing closing ): `(unclosed`
error parsing regexp: invalid named capture: `(?<=a)b`
error parsing regexp: invalid escape sequence: `\1`
^(?:97[89])-?\d{1,5}-?\d+-?\d+-?\d$ shelf- false

Source: regexp and regexp/syntax

Details the output shows:

  • FindAllStringSubmatchIndex returns, for each match, pairs of offsets: the first pair is the whole match and every following pair is one group, -1 when the group did not participate.

  • Named groups are written (?P<name>re) and, since Go 1.22, also (?<name>re). SubexpNames lists them (index 0 is the empty name of the whole match) and SubexpIndex (Go 1.15) maps a name to its position, which is more robust than a hard-coded number.

  • In a replacement template, $member_x means the group named member_x, which does not exist and expands to the empty string. Write ${member}_x. Use $$ for a literal dollar sign.

  • QuoteMeta escapes regular-expression metacharacters, so that a.b*c as user input matches only itself. The quoted pattern correctly fails to match aXbbc.

  • A pattern error is an ordinary error value with the text error parsing regexp: ..., which MustCompile turns into a panic.

Syntax errors

package main

import (
	"fmt"
	"regexp"
)

func main() {
	for _, p := range []string{`a{1001}`, `(?=x)`, `(a)\1`, `*a`, `[z-a]`, `(?i)(?<year>\d{4})`, `\Qa.b\E`, `[[:alpha:]]+`, `a**`} {
		_, err := regexp.Compile(p)
		fmt.Printf("%-18s %v\n", p, err)
	}
}
a{1001}            error parsing regexp: invalid repeat count: `{1001}`
(?=x)              error parsing regexp: invalid or unsupported Perl syntax: `(?=`
(a)\1              error parsing regexp: invalid escape sequence: `\1`
*a                 error parsing regexp: missing argument to repetition operator: `*`
[z-a]              error parsing regexp: invalid character class range: `z-a`
(?i)(?<year>\d{4}) <nil>
\Qa.b\E            <nil>
[[:alpha:]]+       <nil>
a**                error parsing regexp: invalid nested repetition operator: `**`

Source: regexp/syntax

The messages explain what the engine refuses: backreferences, lookahead and lookbehind (invalid or unsupported Perl syntax), repetition counts above 1000, and nested repetition operators. \Q...\E quoting and POSIX classes such as are supported. Under (?i), (?s), (?m) and (?U) the flags i (ignore case), s (dot matches newline), m (multi-line anchors) and U (swap greedy and lazy) change the meaning of the rest of the group.

Linear time, leftmost-first and Unicode

package main

import (
	"fmt"
	"regexp"
	"strings"
	"time"
)

func main() {
	// The classic catastrophic pattern for a backtracking engine.
	re := regexp.MustCompile(`^(a+)+$`)
	for _, n := range []int{20, 200, 20000} {
		input := strings.Repeat("a", n) + "!"
		start := time.Now()
		ok := re.MatchString(input)
		took := time.Since(start)
		fmt.Printf("n=%-6d matched=%-5v fast=%v\n", n, ok, took < 2*time.Second)
	}

	// Leftmost-first (Perl-like) versus leftmost-longest (POSIX) semantics.
	s := "abcd"
	fmt.Println(regexp.MustCompile(`a|ab|abc`).FindString(s))
	fmt.Println(regexp.MustCompilePOSIX(`a|ab|abc`).FindString(s))
	long := regexp.MustCompile(`a|ab|abc`)
	long.Longest()
	fmt.Println(long.FindString(s))

	// An empty match is possible; All functions skip an empty match adjacent to a previous match.
	fmt.Printf("%q\n", regexp.MustCompile(`a*`).FindAllString("baaab", -1))

	// Unicode classes and word boundaries (ASCII only for \b and \w).
	fmt.Println(regexp.MustCompile(`\p{Greek}+`).FindString("alpha = αβγ"))
	fmt.Println(regexp.MustCompile(`\w+`).FindAllString("café naïve", -1))
	fmt.Println(regexp.MustCompile(`[\p{L}]+`).FindAllString("café naïve", -1))
	fmt.Println(regexp.MustCompile(`\bgo\b`).MatchString("go!"), regexp.MustCompile(`\bgo\b`).MatchString("gopher"))
}
n=20     matched=false fast=true
n=200    matched=false fast=true
n=20000  matched=false fast=true
a
abc
abc
["" "aaa" ""]
αβγ
[caf na ve]
[café naïve]
true false

The nested-quantifier pattern ^(a+)+$ is the standard demonstration of catastrophic backtracking elsewhere; here every input is rejected immediately, even with twenty thousand characters. Other facts from the example:

  • By default the engine is leftmost-first like Perl: the first alternative that leads to a match wins, so a|ab|abc matches a. MustCompilePOSIX and Longest() select leftmost-longest semantics, giving abc.

  • \w, \d, \s and \b are ASCII-only. To match letters in any script use \p{L} (or [\p{L}]), as in the last lines of the output; \w+ splits café at the accent.

  • Regexp values are immutable once compiled (calling Longest before sharing them is the exception). Regexp.Copy is deprecated since Go 1.12 because sharing is already safe.

  • Regular expressions are the wrong tool for parsing nested structures (HTML, JSON, source code) and for tasks a strings.Cut or strings.Fields already solves; they are also slower than the plain string functions, so keep them for genuinely irregular text.

Tokenizing: text/scanner and bufio.Scanner

text/scanner is a lexical scanner for UTF-8 text with the lexical conventions of Go: identifiers, integers, floats, characters, strings, raw strings and comments. Scan returns the next token kind (scanner.Ident, scanner.Int, scanner.Float, scanner.String, scanner.Char, scanner.RawString, scanner.Comment, scanner.EOF, or the rune itself for any other character), and TokenText and Position describe it. Mode selects which tokens are recognized and whether comments are skipped (ScanIdents|ScanInts|...|SkipComments; the default GoTokens skips comments), IsIdentRune customizes identifiers, and Error receives lexical errors. It is a good fit for small configuration languages and query syntaxes; the shelf search box could use it like this:

package main

import (
	"fmt"
	"strings"
	"text/scanner"
)

func main() {
	const query = `author:"Frank Herbert" AND year >= 1965 // classic
	OR title:Dune* /* wildcard */ AND NOT pages < 100.5`

	var s scanner.Scanner
	s.Init(strings.NewReader(query))
	s.Filename = "query"
	s.Error = func(s *scanner.Scanner, msg string) { fmt.Println("scan error:", s.Pos(), msg) }

	for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
		fmt.Printf("%-9s %-8s %-18q %s\n", s.Position, scanner.TokenString(tok), s.TokenText(), kind(tok))
	}

	// Mode and IsIdentRune customize what counts as a token: allow dashes in identifiers.
	var d scanner.Scanner
	d.Init(strings.NewReader("on-loan overdue-30 x"))
	d.IsIdentRune = func(ch rune, i int) bool {
		return (ch == '-' || ch >= '0' && ch <= '9') && i > 0 || ch == '_' || ch >= 'a' && ch <= 'z'
	}
	d.Mode = scanner.ScanIdents
	for tok := d.Scan(); tok != scanner.EOF; tok = d.Scan() {
		fmt.Printf("%q ", d.TokenText())
	}
	fmt.Println()
}

func kind(tok rune) string {
	switch tok {
	case scanner.Ident:
		return "identifier"
	case scanner.Int, scanner.Float:
		return "number"
	case scanner.String:
		return "string"
	}
	return "other"
}
query:1:1 Ident    "author"           identifier
query:1:7 ":"      ":"                other
query:1:8 String   "\"Frank Herbert\"" string
query:1:24 Ident    "AND"              identifier
query:1:28 Ident    "year"             identifier
query:1:33 ">"      ">"                other
query:1:34 "="      "="                other
query:1:36 Int      "1965"             number
query:2:2 Ident    "OR"               identifier
query:2:5 Ident    "title"            identifier
query:2:10 ":"      ":"                other
query:2:11 Ident    "Dune"             identifier
query:2:15 "*"      "*"                other
query:2:32 Ident    "AND"              identifier
query:2:36 Ident    "NOT"              identifier
query:2:40 Ident    "pages"            identifier
query:2:46 "<"      "<"                other
query:2:48 Float    "100.5"            number
"on-loan" "overdue-30" "x"

Source: text/scanner

Operators such as >= come back as two separate tokens (> then =), and * after Dune is not part of the identifier, so the parser above the scanner decides how to combine them. For real programming-language grammars use a parser generator or a hand-written lexer; go/scanner tokenizes Go source itself.

bufio.Scanner reads a stream token by token through a split function. The default ScanLines strips a trailing \n or \r\n; ScanWords splits on white space; ScanRunes yields each rune (replacing invalid UTF-8 with U+FFFD); ScanBytes yields each byte; and you can write your own SplitFunc, whose contract is to return how many bytes to consume, the token, and an error, or (0, nil, nil) to ask for more data.

package main

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"strings"
)

func main() {
	input := "Dune 1965\nSolaris 1961\n\nNeuromancer 1984"

	// Default: ScanLines strips \n and \r\n.
	sc := bufio.NewScanner(strings.NewReader(input))
	for sc.Scan() {
		fmt.Printf("%q ", sc.Text())
	}
	fmt.Println(sc.Err())

	// ScanWords splits around white space; ScanRunes yields one rune at a time.
	ws := bufio.NewScanner(strings.NewReader(input))
	ws.Split(bufio.ScanWords)
	words := 0
	for ws.Scan() {
		words++
	}
	fmt.Println("words:", words)

	rs := bufio.NewScanner(strings.NewReader("aé日"))
	rs.Split(bufio.ScanRunes)
	for rs.Scan() {
		fmt.Printf("%q ", rs.Text())
	}
	fmt.Println()

	// A custom SplitFunc: records separated by semicolons.
	semi := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		if i := bytes.IndexByte(data, ';'); i >= 0 {
			return i + 1, bytes.TrimSpace(data[:i]), nil
		}
		if atEOF && len(bytes.TrimSpace(data)) > 0 {
			return len(data), bytes.TrimSpace(data), nil
		}
		return 0, nil, nil // request more data
	}
	cs := bufio.NewScanner(strings.NewReader("a; b ;c;  d"))
	cs.Split(semi)
	for cs.Scan() {
		fmt.Printf("%q ", cs.Text())
	}
	fmt.Println()

	// Lines longer than the buffer stop the scan with ErrTooLong; raise the limit with Buffer.
	long := strings.Repeat("x", 70_000)
	small := bufio.NewScanner(strings.NewReader(long))
	for small.Scan() {
	}
	fmt.Println(small.Err(), errors.Is(small.Err(), bufio.ErrTooLong))

	big := bufio.NewScanner(strings.NewReader(long))
	big.Buffer(make([]byte, 0, 64*1024), 1<<20)
	for big.Scan() {
		fmt.Println("line of", len(big.Text()), "bytes")
	}
	fmt.Println(big.Err())
	fmt.Println(bufio.MaxScanTokenSize)
}
"Dune 1965" "Solaris 1961" "" "Neuromancer 1984" <nil>
words: 6
"a" "é" "日"
"a" "b" "c" "d"
bufio.Scanner: token too long true
line of 70000 bytes
<nil>
65536

The two failure modes that matter in practice are visible above. First, a token longer than the buffer (64 KiB, bufio.MaxScanTokenSize, by default) stops the scan, Scan returns false, and Err() reports bufio.Scanner: token too long; call Buffer(buf, max) before the first Scan to allow longer lines. Second, the loop for sc.Scan() {} ends both at the end of the input and on an error, so always check sc.Err() afterwards: the end of the input is not an error, but a read failure is. The scanner is also not safe for concurrent use. Readers and writers with bufio, reading whole files and os.Stdin handling are in I/O, Files and the Filesystem.

Aligning columns: text/tabwriter

text/tabwriter implements elastic tab stops: text is written with tab-terminated cells, and the writer computes each column’s width from the widest cell, so you do not hard-code widths. tabwriter.NewWriter(out, minwidth, tabwidth, padding, padchar, flags) creates it, and Flush must be called at the end. Compare it with plain fmt widths:

package main

import (
	"fmt"
	"os"
	"strings"
	"text/tabwriter"
)

type Row struct {
	Title  string
	Author string
	Copies int
	Price  float64
}

func main() {
	rows := []Row{
		{"Dune", "Frank Herbert", 3, 9.99},
		{"The Left Hand of Darkness", "Ursula K. Le Guin", 1, 7.5},
		{"日本の昔話", "Anonymous", 2, 12},
	}

	// fmt pads by rune count with widths you must choose by hand.
	for _, r := range rows {
		fmt.Printf("%-27s|%-18s|%3d|%7.2f\n", r.Title, r.Author, r.Copies, r.Price)
	}
	fmt.Println(strings.Repeat("-", 40))

	// tabwriter computes the column widths from the cells. It also counts runes,
	// so double-width East Asian characters still look misaligned in a terminal.
	tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight|tabwriter.Debug)
	fmt.Fprintln(tw, "Title\tAuthor\tCopies\tPrice\t")
	for _, r := range rows {
		fmt.Fprintf(tw, "%s\t%s\t%d\t%.2f\t\n", r.Title, r.Author, r.Copies, r.Price)
	}
	tw.Flush()
}
Dune                       |Frank Herbert     |  3|   9.99
The Left Hand of Darkness  |Ursula K. Le Guin |  1|   7.50
日本の昔話                      |Anonymous         |  2|  12.00
----------------------------------------
                      Title|             Author|  Copies|  Price|
                       Dune|      Frank Herbert|       3|   9.99|
  The Left Hand of Darkness|  Ursula K. Le Guin|       1|   7.50|
                      日本の昔話|          Anonymous|       2|  12.00|

Source: text/tabwriter

The first block needs widths chosen by hand (27 and 18 above), the second adapts to any data, with AlignRight for numeric columns and Debug to draw the column separators. Both count runes, not display cells, so double-width characters do not align in a terminal; for serious terminal tables use a library that measures display width (see the Bubble Tea examples in Go and Rust frameworks for CLI agents).

Compile errors and vet reports

The compiler and go vet produce a small set of messages for the APIs on this page. Each of the following was produced by building or vetting a small program (error demonstration, not code to keep).

A multi-value function used in a single-value context and a type mismatch:

# example.com/shelf/ce
ce/main.go:10:9: assignment mismatch: 1 variable but strings.Cut returns 3 values
ce/main.go:11:7: assignment mismatch: 1 variable but strconv.Atoi returns 2 values
ce/main.go:12:17: invalid operation: fmt.Sprintf("%d", 3) + 4 (mismatched types string and untyped int)

A call whose result is thrown away is legal for the compiler, but go vet reports it for fmt.Sprintf:

ce/main.go:6:2: result of fmt.Sprintf call not used

Other messages you will meet:

Message Cause and fix

fmt.Printf format %d has arg "Dune" of wrong type string

A verb that does not match the operand type (printf analyzer).

fmt.Println call has possible Printf formatting directive %d

A % directive in a Print/Println call that never formats; use Printf.

fmt.Println arg list ends with redundant newline

A trailing \n in a Println argument; remove it.

fmt.Sprintf format %s with arg s causes recursive (T).String method call

A String method formatting its own receiver (see the recursion trap).

conversion from int to string yields a string of one rune, not a string of digits

string(n) on an integer (stringintconv); use strconv.Itoa or string(rune(n)).

strings.CutLast requires go1.27 or later (module is go1.26)

Using a newer standard-library API than the module’s go line (stdversion).

fmt.Errorf format %w has arg ... of wrong type

%w with an operand that is not an error.

missing argument to repetition operator

A regular expression error returned by regexp.Compile or raised by MustCompile.

Choosing between the tools

You have Reach for

"key=value" or "host:port"

strings.Cut (or CutLast for the final separator).

Words separated by arbitrary white space

strings.Fields or FieldsSeq.

A comma-separated record that never contains quoted commas

strings.Split; real CSV needs encoding/csv.

A number in a string

strconv.Atoi, ParseInt or ParseFloat, never fmt.Sscan in a loop.

A short, rigid line with several typed fields

fmt.Sscanf, accepting its leniency.

A pattern with captures, alternatives or character classes

regexp, compiled once.

Many appends to a string

strings.Builder with Grow.

Formatting for people (reports, tables)

fmt with widths, or text/tabwriter.

Formatting for machines (logs, wire formats)

log/slog, encoding/json, strconv.Append*; not Sprintf.

User-supplied text in an error or log

%q.

Where older material is outdated

Books and tutorials written for Go 1.5 to 1.12, among them the two this reference consulted (Bodner’s first edition and Vivien’s first edition), describe the print and scan families of fmt, bufio.Scanner, bytes.Buffer and strings.Reader accurately, and those parts are still correct. What they predate:

  • strings.Cut (1.18), CutPrefix/CutSuffix (1.20) and CutLast (1.27) replace the Index-then-slice and SplitN(s, sep, 2) idioms.

  • fmt.Append* (1.19), fmt.FormatString (1.20), several %w verbs in fmt.Errorf (1.20), and %w itself (1.13), which older error chapters replace with == comparisons of sentinel errors or a string match on err.Error().

  • strings.Builder (1.10) instead of bytes.Buffer for building strings; strings.Title is deprecated (1.18).

  • Iterator functions Lines, SplitSeq, FieldsSeq and FieldsFuncSeq in strings and bytes (1.24).

  • regexp gained (?<name>...) groups (1.22) and Regexp.MarshalText/UnmarshalText (1.21); Copy was deprecated (1.12).

  • bufio.Scanner and the 64 KiB token limit are described in the books, but Scanner.Buffer and the ErrTooLong handling are often omitted.

  • bytes.Buffer.Peek (1.26); unicode.Version "17.0.0" and the Unicode 17 tables (1.27).

  • Stringer belongs to fmt; it is not a built-in interface.

Version notes

Go Change

1.10

strings.Builder.

1.12

fmt prints maps with sorted keys; Regexp.Copy deprecated.

1.13

%w in fmt.Errorf; strings.ToValidUTF8.

1.14

(*strconv.NumError).Unwrap.

1.15

Regexp.SubexpIndex; go vet reports string(int).

1.17

strconv.QuotedPrefix.

1.18

strings.Cut, bytes.Cut, strings.Clone; strings.Title deprecated.

1.19

fmt.Append, Appendf, Appendln.

1.20

strings.CutPrefix and CutSuffix, several %w verbs, fmt.FormatString.

1.21

Regexp.MarshalText and UnmarshalText.

1.22

(?<name>re) named groups.

1.24

strings and bytes Lines, SplitSeq, SplitAfterSeq, FieldsSeq, FieldsFuncSeq; Regexp.AppendText.

1.26

bytes.Buffer.Peek.

1.27

strings.CutLast and bytes.CutLast; Unicode 17 tables and unicode.Version "17.0.0".

Go 1.28 is not yet released (expected February 2027), so this page describes only the APIs of Go 1.27 and earlier.