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

ConceptIn Go
StringImmutable byte sequence, no \0, len is O(1)
byte vs runeuint8 (one octet) vs int32 (one code point)
Byte vs char countlen(s) vs utf8.RuneCountInString(s)
Walk charactersfor i, r := range s decodes UTF-8
Edit textConvert to []rune, change, convert back
Build in a loopstrings.Builder, not +=

Next: 03 — Structs & Methods.