02 — Strings & Runes
What a Go string really is, the difference between a byte and a
rune, how UTF-8 connects them, and how to build strings efficiently. Every
example is live — edit it and press Run.
1. Bytes vs code points
A Go string is an immutable, length-carrying sequence of
bytes — no \0 terminator, and len(s) is the
byte count (O(1)). Crucially, bytes are not characters. Go stores text as
UTF-8, where a code point takes 1–4 bytes, so for non-ASCII text the
byte count and the character count differ:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "héllo" // é is 2 bytes in UTF-8
fmt.Println(len(s)) // bytes
fmt.Println(utf8.RuneCountInString(s)) // code points
}
Output:
6
5
2. Ranging decodes UTF-8
Indexing a string (s[i]) gives you a raw byte. But
for ... range over a string decodes UTF-8: each iteration
yields the byte index where a character starts and the rune (code point)
itself. A rune is an alias for int32.
package main
import "fmt"
func main() {
s := "go🚀" // the rocket is one rune, 4 bytes
for i, r := range s {
fmt.Printf("%d: %c (%d)\n", i, r, r)
}
}
Output:
0: g (103)
1: o (111)
2: 🚀 (128640)
Note the index jumps from 2 straight past 3–5: the rocket occupies bytes 2–5, so the next character (if any) would start at index 6.
3. Editing text: convert to []rune
Strings are immutable, so to transform text you convert to a mutable slice and back. For
anything beyond ASCII, convert to []rune so you work one character
at a time — reversing the raw bytes would corrupt multi-byte characters.
package main
import "fmt"
func ReverseRunes(s string) string {
rs := []rune(s) // decode into code points
for i, j := 0, len(rs)-1; i < j; i, j = i+1, j-1 {
rs[i], rs[j] = rs[j], rs[i]
}
return string(rs) // re-encode to UTF-8
}
func main() {
fmt.Println(ReverseRunes("héllo"))
fmt.Println(ReverseRunes("go🚀"))
}
Output:
olléh
🚀og
4. Building strings: strings.Builder
Because strings are immutable, a + b allocates a new string every time —
doing it in a loop is quadratic, like repeated strcat.
strings.Builder is a growable buffer that amortises the cost, handing you
the finished string once:
package main
import (
"fmt"
"strings"
)
func Repeat(s string, n int) string {
var b strings.Builder
for i := 0; i < n; i++ {
b.WriteString(s)
}
return b.String()
}
func main() {
fmt.Println(Repeat("ab", 3))
}
Output:
ababab
Recap
| Concept | In Go |
|---|---|
| String | Immutable byte sequence, no \0, len is O(1) |
byte vs rune | uint8 (one octet) vs int32 (one code point) |
| Byte vs char count | len(s) vs utf8.RuneCountInString(s) |
| Walk characters | for i, r := range s decodes UTF-8 |
| Edit text | Convert to []rune, change, convert back |
| Build in a loop | strings.Builder, not += |
Next: 03 — Structs & Methods.