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 |
|
Verbs, flags, width and precision; |
Parse text that follows a fixed layout |
|
Convenient, forgiving about nothing; prefer |
Split, join, search, trim, replace |
|
Everything is byte-oriented, UTF-8 aware where it needs to be. |
Build a string piece by piece |
|
Avoids the quadratic copying of |
Numbers to and from text |
|
Returns |
Character classes and scripts |
|
Unicode 17 tables since Go 1.27. |
Pattern matching with captures |
|
RE2 syntax: no backreferences or lookaround, but linear time in the input. |
Tokenize source-like text |
|
Go-flavoured lexer for identifiers, numbers, strings and comments. |
Read lines, words or custom records |
|
64 KiB token limit unless you raise it with |
Align columns |
|
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 ( |
|
|
|
format string |
|
|
|
default formatting, spaces always, newline at the end |
|
|
|
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
Source: fmt: Printing and Effective Go: Printing
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 |
|---|---|
|
The operand in its default format. Structs print as |
|
Like |
|
A Go-syntax representation that would compile: |
|
The Go-syntax type of the operand: |
|
A literal percent sign; consumes no operand. |
The remaining verbs depend on the kind of operand:
| Operand | Verbs | Meaning |
|---|---|---|
boolean |
|
|
integer |
|
Base 10, 2, 8, 8 with |
integer |
|
The character with that code point, the single-quoted Go literal for it, and |
float and complex |
|
Scientific, decimal point, and the shortest of the two ( |
string and |
|
The bytes as text, a double-quoted Go literal with escapes, and two hexadecimal digits per byte. |
slice |
any element verb, plus |
The verb is applied to every element: |
pointer |
|
Hexadecimal address; |
error or |
|
The |
Flags and modifiers go between the % and the verb:
| Modifier | Effect |
|---|---|
|
Always print a sign for numbers; with |
|
Pad on the right (left-justify) instead of the left. |
|
Alternate format: |
space |
Leave a space for the sign of numbers ( |
|
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: |
|
For floats the digits after the decimal point ( |
|
Take the width or precision from the next |
|
An explicit argument index (1-based) before a verb or a |
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 ]
Source: fmt: Printing and fmt: Explicit argument indexes
Some rules from that output that surprise newcomers:
-
%xon an integer formats the number; on a string or[]byteit formats every byte, and% xinserts a space between bytes.%Xgives upper-case digits. Both%xand%qon a type that implementsStringerorerrorformat the string returned by the method, not the underlying integer (see below). -
Width is a minimum and counts runes, so
%-6sof"日本"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 (
%.2sof"Dune"isDu); on a float it rounds (%.0fof2.5is2, because the exact binary value is ties-to-even).%gwith a precision counts significant digits, and%vof afloat64uses%gwith the shortest representation that round-trips (1e+06,1e+21). -
Money should not be stored in a
float64just because%.2fmakes it look right; use integer cents and format them yourself (see theFormatterexample below). -
%+dprints a sign even for positive numbers,% dleaves a space for it, and%08.3fzero-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 |
|---|---|
|
The verb |
|
The format has more verbs than the call has operands. |
|
The call has more operands than the format has verbs. |
|
Unknown verb |
|
The operand for |
|
An explicit argument index is out of range. |
|
A |
|
The operand’s |
|
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
Source: fmt: Format errors and cmd/vet
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:
-
Formatter(Format(f fmt.State, verb rune)) takes over completely for every verb. -
For the verbs that print strings (
%v,%s,%q,%x,%X) and forPrint/Println: if the operand implementserror,Error()is used; otherwise if it implementsStringer(String() string),String()is used. -
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>
Source: fmt: Stringer, fmt: GoStringer and fmt: Printing
The output teaches several rules at once:
-
Stringerapplies to nested values too. TheBookis printed as{978-0-441172719 Dune on loan}, with theISBNandLoanStatefields rendered by theirStringmethods, becausefmtcalls the method on every exported field (or interface element) it reaches; it cannot call methods on unexported fields, which are printed raw.%dstill prints the underlying number, while%xof aLoanStatesurprisingly hexadecimal-encodes the string returned byString(6f6e206c6f616e), since%xis a string verb as soon as the operand is aStringer. -
%#vignoresStringand shows the Go syntax, unlessGoStringis defined, as it is here forLoanState. -
Method sets matter.
MemberdefinesStringon*Member, sofmt.Println(m)with aMembervalue prints{Ana}, butPrintln(&m)printsmember Ana. A slice[]*Membertherefore uses the method and[]Memberdoes not. Prefer a value receiver forStringunless the type must not be copied (see Methods and Receivers). -
A type that implements both
errorandStringerprints throughError(); theStringmethod ofNotFoundis never used by%v. -
If
Stringpanics,fmtcatches the panic and prints%!v(PANIC=String method: ...). If the receiver is anilpointer, 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
Source: fmt: Formatter, fmt: State and fmt: FormatString
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:
-
Scantreats newlines as ordinary white space, so operands may span lines. -
Scanlnstops at a newline, and after the last operand there must be a newline or end of input. -
Scanfmatches 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:
-
Sscanfwith a mismatching input reportsexpected integerand the count of operands already assigned (two), which is why theLoanvalue 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 bothnanderr. -
Sscanln("1\n2", &a, &b)fails withunexpected newline:Scanlndoes not look past the end of the line. -
Fscanon a reader that does not implementio.RuneScanner(it needsUnreadRune) may lose one character of look-ahead between calls. Wrap files and network connections in abufio.Readerbefore 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.TextUnmarshaleris 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
Source: strings: Cut, strings: CutLast, strings: CutPrefix and Go 1.27 release notes
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
Source: strings: Split, strings: Fields and strings: EqualFold
Reading the output:
-
Split("", ",")returns a slice with one empty string, not an empty slice, solen(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;Fieldsis the right tool. -
EqualFolduses simple Unicode case folding: it knows"Go"equals"GO", but not that"ß"expands to"SS". Locale-correct comparison needsgolang.org/x/text. -
Count(s, "")returns one more than the number of runes. -
Compareexists for symmetry withbytes.Compare; the comparison operators<and==on strings do the same job and are usually clearer. For sorting useslices.Sortorcmp.Compare.
Searching, prefixes and case
| Functions | Purpose |
|---|---|
|
Report whether the string holds a substring, any rune from a set, one rune, or a rune satisfying a predicate. |
|
Test an affix; use |
|
Byte offset of the first (or last) match, |
|
Non-overlapping occurrences. |
|
Rune-by-rune case mapping; the |
|
Case-insensitive equality with simple folding. |
|
Deprecated since Go 1.18: its word-boundary rule does not handle Unicode punctuation. Use |
|
Remove characters from the ends; the cutset forms take a set, the |
|
Substitution of one or many substrings. |
|
Copy a string so that a small substring does not keep a huge backing array alive (Go 1.18). |
|
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
Source: strings: Builder
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("<", "<", ">", ">", "&", "&")
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 < b && c > d
if a < b && c > d
baab aaaa
<done>
true 6 6
Source: strings: Replacer, strings: Clone and html: EscapeString
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
Source: strings: SplitSeq, strings: Lines and Go 1.24 release notes
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
Source: bytes: Buffer, bytes: CutLast and io: ReadAll
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 |
|---|---|
|
Base-10 |
|
|
|
|
|
Accept |
|
Complex numbers such as |
|
|
|
Append the text to a |
|
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"
Source: strconv: Numeric Conversions and strconv: NumError
Points to take from the output:
-
On a range error
ParseIntreturns the clamped value (127for an 8-bit parse of300) 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 withfmt.Errorf("pages: %w", err)keeps both the context and theerrors.Isbehaviour. -
Atoidoes not trim white space; callstrings.TrimSpacefirst when the input comes from a user. -
ParseFloat("0.1", 32)returns afloat64holding the nearestfloat32; converting it withfloat32(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 whatfmtitself 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`
Source: strconv: String Conversions and strconv: Quote
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 é
Source: unicode and unicode: Version
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 whilestrconv.Atoi("٣")fails: classification and parsing are different questions. To accept only ASCII digits writer >= '0' && r <= '9'. -
unicode.ToUpper('é')is'É'butstrings.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ßintoSS. Language-sensitive case conversion (Turkish dotlessi, Greek final sigma) needsunicode.SpecialCaseorgolang.org/x/text/cases. -
A "character" as a user sees it may be several runes (
eplus a combining accent is two runes and three bytes here), so neitherlennorutf8.RuneCountInStringcounts characters. Normalization and grapheme-cluster segmentation are ingolang.org/x/text/unicode/normand 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:
-
FindAllStringSubmatchIndexreturns, for each match, pairs of offsets: the first pair is the whole match and every following pair is one group,-1when the group did not participate. -
Named groups are written
(?P<name>re)and, since Go 1.22, also(?<name>re).SubexpNameslists them (index 0 is the empty name of the whole match) andSubexpIndex(Go 1.15) maps a name to its position, which is more robust than a hard-coded number. -
In a replacement template,
$member_xmeans the group namedmember_x, which does not exist and expands to the empty string. Write${member}_x. Use$$for a literal dollar sign. -
QuoteMetaescapes regular-expression metacharacters, so thata.b*cas user input matches only itself. The quoted pattern correctly fails to matchaXbbc. -
A pattern error is an ordinary error value with the text
error parsing regexp: ..., whichMustCompileturns 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
Source: regexp: Syntax and regexp: Longest
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|abcmatchesa.MustCompilePOSIXandLongest()select leftmost-longest semantics, givingabc. -
\w,\d,\sand\bare ASCII-only. To match letters in any script use\p{L}(or[\p{L}]), as in the last lines of the output;\w+splitscaféat the accent. -
Regexpvalues are immutable once compiled (callingLongestbefore sharing them is the exception).Regexp.Copyis 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.Cutorstrings.Fieldsalready 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
Source: bufio: Scanner and bufio: SplitFunc
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 |
|---|---|
|
A verb that does not match the operand type ( |
|
A |
|
A trailing |
|
A |
|
|
|
Using a newer standard-library API than the module’s |
|
|
|
A regular expression error returned by |
Choosing between the tools
| You have | Reach for |
|---|---|
|
|
Words separated by arbitrary white space |
|
A comma-separated record that never contains quoted commas |
|
A number in a string |
|
A short, rigid line with several typed fields |
|
A pattern with captures, alternatives or character classes |
|
Many appends to a string |
|
Formatting for people (reports, tables) |
|
Formatting for machines (logs, wire formats) |
|
User-supplied text in an error or log |
|
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) andCutLast(1.27) replace theIndex-then-slice andSplitN(s, sep, 2)idioms. -
fmt.Append*(1.19),fmt.FormatString(1.20), several%wverbs infmt.Errorf(1.20), and%witself (1.13), which older error chapters replace with==comparisons of sentinel errors or a string match onerr.Error(). -
strings.Builder(1.10) instead ofbytes.Bufferfor building strings;strings.Titleis deprecated (1.18). -
Iterator functions
Lines,SplitSeq,FieldsSeqandFieldsFuncSeqinstringsandbytes(1.24). -
regexpgained(?<name>...)groups (1.22) andRegexp.MarshalText/UnmarshalText(1.21);Copywas deprecated (1.12). -
bufio.Scannerand the 64 KiB token limit are described in the books, butScanner.Bufferand theErrTooLonghandling are often omitted. -
bytes.Buffer.Peek(1.26);unicode.Version"17.0.0"and the Unicode 17 tables (1.27). -
Stringerbelongs tofmt; it is not a built-in interface.
Version notes
| Go | Change |
|---|---|
1.10 |
|
1.12 |
|
1.13 |
|
1.14 |
|
1.15 |
|
1.17 |
|
1.18 |
|
1.19 |
|
1.20 |
|
1.21 |
|
1.22 |
|
1.24 |
|
1.26 |
|
1.27 |
|
Go 1.28 is not yet released (expected February 2027), so this page describes only the APIs of Go 1.27 and earlier.