Strings and Runes
A Go string is a read-only slice of bytes, not characters. Understanding that distinction prevents a specific and common category of bug.
s := "Hello, 世界"
fmt.Println(len(s)) // 13, not 9 — that is BYTESA Go string is an immutable sequence of bytes, conventionally holding UTF-8. len gives you the byte count, and indexing gives you a byte:
fmt.Println(s[0]) // 72 — a byte, not "H"
fmt.Println(string(s[0])) // "H"
fmt.Println(s[7]) // 228 — the first byte of 世, meaningless aloneThat is the source of the bug: slicing a string at an arbitrary byte offset can cut a multi-byte character in half.
Runes#
A rune is an alias for int32 and holds one Unicode code point.
fmt.Println(utf8.RuneCountInString(s)) // 9 — actual characters
runes := []rune(s)
fmt.Println(len(runes)) // 9
fmt.Println(string(runes[7])) // 世Converting to []rune allocates and copies, so do it when you need character-level indexing, not in a hot loop.
Ranging over a string#
range decodes UTF-8 for you, which is almost always what you want:
for i, r := range s {
fmt.Printf("%d: %c\n", i, r)
}
// i is the BYTE index, r is a rune
// 0: H ... 7: 世 10: 界Note the index jumps from 7 to 10 — 世 occupies three bytes. Ranging is the safe way to walk a string; for i := 0; i < len(s); i++ walks bytes and will split characters.
Immutability#
s[0] = 'h' // compile error: cannot assignStrings cannot be modified. To change one, build a new one:
b := []byte(s)
b[0] = 'h'
s = string(b)Both conversions copy. That is why concatenating in a loop is expensive.
Building strings#
// BAD: quadratic — each += allocates and copies the whole string
var out string
for _, w := range words {
out += w + " "
}
// GOOD
var b strings.Builder
b.Grow(64) // optional, avoids regrowth
for _, w := range words {
b.WriteString(w)
b.WriteByte(' ')
}
out := b.String()
// BEST when you already have the slice
out := strings.Join(words, " ")This is the most common performance mistake in generated Go — see the performance page.
The strings package#
strings.Contains(s, "ell") // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "界") // true
strings.Index(s, "o") // 4, or -1
strings.Split("a,b,c", ",") // []string{"a","b","c"}
strings.Join([]string{"a","b"}, "-")// "a-b"
strings.TrimSpace(" hi ") // "hi"
strings.Trim("xxhixx", "x") // "hi"
strings.ReplaceAll("a-b-c", "-", "+")
strings.ToUpper(s)
strings.Fields(" a b ") // []string{"a","b"} — splits on any whitespace
strings.EqualFold("Go", "GO") // true — case-insensitive compare
strings.Cut("key=value", "=") // "key", "value", truestrings.Cut is the modern way to split on the first occurrence and is clearer than Index plus slicing.
Converting to and from numbers#
n, err := strconv.Atoi("42") // string -> int
s := strconv.Itoa(42) // int -> string
f, err := strconv.ParseFloat("3.14", 64)
b, err := strconv.ParseBool("true")
s := strconv.FormatInt(255, 16) // "ff"Formatting#
fmt.Sprintf("%s is %d years old", name, age)
fmt.Sprintf("%.2f", 3.14159) // "3.14"
fmt.Sprintf("%v", someStruct) // default representation
fmt.Sprintf("%+v", someStruct) // with field names
fmt.Sprintf("%#v", someStruct) // Go syntax
fmt.Sprintf("%T", someValue) // the type
fmt.Sprintf("%q", "hi") // `"hi"` — quoted%+v is the one to remember for debugging structs, and %T for "what actually is this".
Exercise#
package main
import "fmt"
func main() {
phrase := "Go is 简单"
// Print, one per line:
// the byte length
// the rune count
// each rune with its byte index, using range
// the phrase reversed BY RUNE (not by byte)
fmt.Println(phrase)
}Common questions#
Why is len() giving me the wrong length?#
It is giving you the byte length, which differs from the character count for any non-ASCII text. Use utf8.RuneCountInString for characters, and range to iterate them safely.
When should I use []byte instead of string?#
When you are building or mutating data, doing I/O, or working with a package that expects bytes. Strings are immutable so every modification copies; []byte avoids that. Converting between them copies, so pick one representation and stay in it within a hot path.
Is strings.Builder worth it for a few concatenations?#
No — for two or three, + is clearer and the difference is irrelevant. It matters inside loops, where += is quadratic because each step copies everything accumulated so far.
Get the Go agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.