A Go string is an immutable, read-only slice of bytes; by convention (not enforcement) that byte sequence holds UTF-8 encoded text. A rune (an alias for int32) represents a single Unicode code point. Because UTF-8 encodes a code point in anywhere from 1 to 4 bytes, indexing a string with s[i] gives you one raw byte, which is only ever the whole character for plain ASCII text.
Indexing gives bytes, not characters
Because a string is a byte slice underneath, s[i] yields a single byte (a uint8) at that byte offset — for ASCII text this happens to match each character, but for any multi-byte UTF-8 character it splits it apart.
s := "café" // é is 2 bytes in UTF-8
fmt.Println(len(s)) // 5 — byte length, not character count (4 characters)
fmt.Println(s[3]) // 195 — a raw byte, half of é's 2-byte encoding, not 'é'range over a string gives runes
Unlike plain indexing, for range over a string decodes the UTF-8 correctly: it gives you the byte offset where each rune starts, and the decoded rune itself (as an int32 code point) — this is the idiomatic way to iterate character-by-character.
for i, r := range "café" {
fmt.Printf("%d: %c\n", i, r)
}
// 0: c
// 1: a
// 2: f
// 3: é — correctly decoded as ONE rune, even though it's 2 bytes
fmt.Println(len([]rune("café"))) // 4 — the actual character countConverting between strings, bytes, and runes
Converting a string to []byte gives the raw UTF-8 bytes; converting to []rune decodes it into Unicode code points, which is what you want when you need an accurate character count or to index by character rather than by byte. Both conversions copy the data, since strings are immutable.
s := "héllo"
b := []byte(s) // raw UTF-8 bytes, len 6
r := []rune(s) // decoded code points, len 5 — the real character count
back := string(r) // convert back to a string: "héllo"