Strings, bytes and runes¶
The basic types article
established what a string is: an immutable sequence of bytes, usually
holding UTF-8. This one is about the packages you actually reach for —
strings, bytes and unicode/utf8 — and the places where the byte/
character distinction bites.
s := " Name: Ada Lovelace "
fmt.Printf("%q\n", strings.TrimSpace(s)) // output: "Name: Ada Lovelace"
%q prints the string in quotes, so it is easy to see that the spaces are gone.
Searching¶
fmt.Println(strings.Contains("chicken", "ken")) // output: true
fmt.Println(strings.HasPrefix("chicken", "chi")) // output: true
fmt.Println(strings.Index("chicken", "ken")) // output: 4
fmt.Println(strings.Index("chicken", "zz")) // output: -1
Index returns a byte offset, and -1 when there is no match. It
never fails, so there is nothing to handle.
Cut is usually what you want¶
Splitting "key: value" into two parts is common enough to have its own function, and it tells you whether the separator was there at all:
key, val, found := strings.Cut("Name: Ada", ": ")
fmt.Printf("%q %q %v\n", key, val, found) // output: "Name" "Ada" true
_, _, found = strings.Cut("noseparator", ": ")
fmt.Println(found) // output: false
Without the found result you cannot distinguish "separator missing"
from "value was empty" — which is exactly the bug Cut exists to
prevent.
Split and Fields are not the same¶
Split cuts on an exact separator and keeps empty pieces. Fields
splits on runs of whitespace and discards them:
fmt.Printf("%q\n", strings.Split("a,b,,c", ",")) // output: ["a" "b" "" "c"]
fmt.Printf("%q\n", strings.Fields(" a b \t c\n"))
// output: ["a" "b" "c"]
Reach for Split when the separator is data (a CSV field, a path) and
Fields when it is just spacing. Using Split(s, " ") on
human-formatted text gives you a slice full of empty strings:
Building and replacing¶
fmt.Println(strings.Join([]string{"a", "b", "c"}, "-")) // output: a-b-c
fmt.Println(strings.Repeat("ab", 3)) // output: ababab
fmt.Println(strings.ReplaceAll("a.b.c", ".", "/")) // output: a/b/c
fmt.Println(strings.Replace("a.b.c", ".", "/", 1)) // output: a/b.c
For several replacements in one pass, strings.NewReplacer beats
chained ReplaceAll calls — it walks the input once, and the value is
safe to reuse and to share between goroutines:
r := strings.NewReplacer("<", "<", ">", ">")
fmt.Println(r.Replace("<b>hi</b>")) // output: <b>hi</b>
strings.Builder¶
The operators article showed why
s += x in a loop is O(n²). Builder is the fix, and because it
implements io.Writer you can also print straight into it:
var b strings.Builder
for i := range 3 {
fmt.Fprintf(&b, "%d,", i)
}
fmt.Println(b.String()) // output: 0,1,2,
Note &b — the Write methods have pointer receivers, so a Builder
must not be copied after first use.
The bytes package mirrors strings¶
Almost every function in strings has a bytes twin with the same name
that works on []byte. Use it when the data arrives as bytes — from a
file, a socket, a request body — so you avoid converting to string and
back, since each conversion copies:
bb := []byte("hello")
fmt.Println(bytes.Contains(bb, []byte("ell"))) // output: true
fmt.Println(string(bytes.ToUpper(bb))) // output: HELLO
The string(...) there is not decoration. bytes.ToUpper returns a
[]byte, and printing one shows the numbers:
The one gap is strings.NewReplacer — there is no bytes.NewReplacer.
It is the only exported function in strings with no counterpart, so
for that case convert, or chain bytes.ReplaceAll.
bytes.Buffer is the []byte counterpart of strings.Builder, and it
is both an io.Writer and an io.Reader:
var buf bytes.Buffer
buf.WriteString("abc")
buf.WriteByte('!')
fmt.Println(buf.String(), buf.Len()) // output: abc! 4
Bytes are not characters¶
len counts bytes. A rune — one Unicode code point — takes one to four
of them:
Ranging over a string decodes runes and gives you the byte offset of each, so the index jumps:
There is no index 2: ö occupies bytes 1 and 2. Which is why slicing
a string slices bytes, and converting to []rune first is what slices
characters:
Converting to []rune allocates a copy, so do it when you genuinely
need character positions, not by reflex.
unicode/utf8 handles the awkward edges — decoding one rune at a time,
and checking that bytes from outside your program are valid UTF-8 at all:
r, size := utf8.DecodeRuneInString("世界")
fmt.Println(string(r), size) // output: 世 3
fmt.Println(utf8.ValidString("ok")) // output: true
fmt.Println(utf8.ValidString(string([]byte{0xff}))) // output: false
Case folding is not simple¶
ToUpper and ToLower map rune by rune, which is not the same as the
casing rules of any particular language:
fmt.Println(strings.ToUpper("größe")) // output: GRÖßE
fmt.Println(strings.ToLower("ÄPFEL")) // output: äpfel
German ß has no single-rune uppercase, so it survives unchanged. For
comparing two strings case-insensitively, do not lowercase both — use
the function built for it:
From Python: a Go
stringis Python'sbyteswith a UTF-8 convention, not Python'sstr.len()differs for exactly that reason,s[0]gives you a byte rather than a one-character string, and[]rune(s)is the closest thing to what Python hands you when you index astr.
Quick reference¶
| Task | Call |
|---|---|
| substring test / position | strings.Contains, strings.Index (byte offset, -1 if absent) |
| split on "key: value" | strings.Cut — returns the found flag too |
| split on a separator | strings.Split (keeps empties) |
| split on whitespace | strings.Fields (drops empties) |
| join / repeat | strings.Join, strings.Repeat |
| many replacements at once | strings.NewReplacer |
| build a string in a loop | strings.Builder, passed as &b |
same operations on []byte |
the bytes package, plus bytes.Buffer |
| count characters | utf8.RuneCountInString (len counts bytes) |
| slice by character | []rune(s)[a:b] (allocates) |
| case-insensitive compare | strings.EqualFold |
| validate external bytes | utf8.ValidString |
Sources¶
stringspackage reference — pkg.go.dev/stringsbytespackage reference — pkg.go.dev/bytesunicode/utf8package reference — pkg.go.dev/unicode/utf8- Go blog: strings, bytes, runes and characters — go.dev/blog/strings
- String types — go.dev/ref/spec#String_types
- For statements with range clause — go.dev/ref/spec#For_range