12. Strings, runes, bytes, and Unicode
12.1 A Go string is UTF-8 bytes, not characters
s := "héllo"
fmt.Println(len(s)) // 6 — 'é' is 2 bytes in UTF-8, not 1
fmt.Println(len([]rune(s))) // 5 — rune count matches human intuition
Indexing a string with s[i] gives you a byte, not a character.
for i, r := range s gives you rune-by-rune iteration with i as the
byte offset of each rune's start (which can skip values for multi-byte
runes) — this trips up almost everyone once.
12.2 Strings are immutable; conversions to []byte copy
b := []byte(s) // allocates and copies
s2 := string(b) // allocates and copies back
Because of that copy cost, hot paths that build strings incrementally
should use strings.Builder (amortized-growth, like append on a
slice) instead of repeated + concatenation:
var sb strings.Builder
for _, part := range parts {
sb.WriteString(part)
sb.WriteByte('\n')
}
result := sb.String() // one final allocation, not N
12.3 The strings, strconv, and unicode/utf8 packages
strings.Split, strings.Join, strings.Contains, strings.TrimSpace,
strconv.Itoa/Atoi, strconv.ParseFloat, and utf8.RuneCountInString
cover the overwhelming majority of text-munging you'll do. Reach for
regular expressions (regexp) only when a fixed-pattern parse genuinely
isn't enough — they're slower and harder to read than they look.
12.4 Real-world example
A CLI that accepts --size=10MB needs to split the numeric prefix from
the unit suffix, strconv.ParseFloat the number, and look up the unit in
the iota-based Unit type from Chapter 3 — three standard-library calls,
no regex needed, and it handles "1.5GB" just as easily as "10MB".