Indexing and looping over strings behaves differently from many languages because Go strings are UTF-8 bytes.
A Go string is an immutable sequence of bytes, conventionally UTF-8 text. Two consequences matter every day:
len(s)is the number of bytes, not characters.len("héllo")is 6 becauseétakes two bytes.s[i]is abyte, and slicings[a:b]slices bytes, which can cut a multi-byte character in half.
A rune (alias for int32) is a Unicode code point. for i, r := range s decodes UTF-8 as it goes, giving the byte offset i and the rune r — use it whenever you mean "each character". utf8.RuneCountInString(s) counts runes, and []rune(s) converts to a slice of runes when you need random access by character.
Strings are immutable, so building one with += in a loop allocates repeatedly. Use strings.Builder (or bytes.Buffer) to build text efficiently, and convert with []byte(s) / string(b) when an API wants bytes (each conversion copies).
The strings package covers most text work: Contains, HasPrefix, Split, Fields, Join, Replace, ToUpper, TrimSpace, Cut. Numbers convert with strconv: strconv.Itoa(42), strconv.Atoi("42"), strconv.ParseFloat. Note that string(65) gives "A" (a rune), not "65" — go vet warns about it.
Raw string literals in backquotes, like a regex or a multi-line template, ignore escape sequences.
Example
package main
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
func main() {
s := "héllo"
fmt.Println(len(s), utf8.RuneCountInString(s)) // 6 5
for i, r := range "hé" {
fmt.Printf("%d:%c ", i, r) // 0:h 1:é
}
fmt.Println()
var b strings.Builder
for i := range 3 {
b.WriteString(strconv.Itoa(i))
}
fmt.Println(b.String()) // 012
key, val, _ := strings.Cut("lang=go", "=")
fmt.Println(key, val) // lang go
}Remember
- Strings are immutable UTF-8 bytes;
lencounts bytes rangeover a string yields runes; indexing yields bytes- Use strings.Builder for building and strconv for numbers
Watch out for
- Using
string(n)to format an integer — usestrconv.Itoa(n)orfmt.Sprint(n).