In Go, strings are read-only slices of bytes. This design has several important implications.
The function len counts the bytes - not the number of characters:
var name string = "Max"
var name2 string = "ÄÜÖ"
fmt.Println(len(name)) // 3
fmt.Println(len(name2)) // 6
// Instead use:
fmt.Println(utf8.RuneCountInString(name2))
fmt.Println(len([]rune(name2)))
String internals
A string is a reflect.StringHeader — a struct with two fields:
type StringHeader struct {
Data uintptr // pointer to underlying byte array
Len int // length in bytes (NOT characters)
}
- No null terminator — the length is stored explicitly, unlike C strings.
- Immutable — you cannot modify a string's bytes after creation. Any "modification" allocates a new string.
Slicing is O(1)
Because a string is just a pointer + length, slicing shares the underlying data:
s := "Hello, World"
sub := s[0:5] // "Hello" — no copy, just a new header
Slicing a large string and keeping a small subslice prevents the entire original string from being garbage collected. Use strings.Clone() if needed.
UTF-8 and Runes
Use []rune to get Unicode code points:
fmt.Println(len([]rune("世界"))) // 2
fmt.Println(utf8.RuneCountInString("世界")) // 2
for range iterates by rune
for i, r := range "世界" {
fmt.Printf("byte offset %d: %c (U+%04X)\n", i, r, r)
}
// byte offset 0: 世 (U+4E16)
// byte offset 3: 界 (U+754C)
Indexing with s[i] gives you a byte, not a character. for range is the safe way to iterate over characters.
String conversions
| From | To | Copy? |
|---|---|---|
string | []byte | Yes (usually) |
string | []rune | Yes |
[]byte | string | Yes (usually) |
[]rune | string | Yes |
The compiler optimizes some conversions to avoid allocation (e.g., map lookups with string(b)), but in general assume a copy.
s := "Hello"
b := []byte(s) // allocates a new byte slice
r := []rune(s) // allocates a new rune slice
Efficient concatenation
Avoid repeated + in loops — each concatenation allocates a new string. Use strings.Builder:
var b strings.Builder
for _, word := range words {
b.WriteString(word)
}
result := b.String()
strings.Builder grows its internal buffer like a slice, so it amortizes allocation cost. Call b.Grow(n) to pre-allocate if you know the approximate size.
Useful packages
- `strings` — search, replace, split, trim, Builder
- `strconv` — convert to/from string (numbers, booleans)
- `fmt` — formatted I/O with
%s,%q,%x unicode/utf8— rune operations, validation, encodingregexp— regular expressions (returns[]byteorstringmatches)
Key takeaways
- Strings are immutable byte slices with explicit length
len()gives bytes; useutf8.RuneCountInString()for character countfor rangeiterates runes;s[i]indexes bytes- Use
strings.Builderfor efficient concatenation - Slicing shares memory — use
strings.Clone()to avoid retaining large strings