2 min read 499 words Updated Sep 24, 2026 Created Sep 24, 2026
#Go#Programming

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
Memory leak risk

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)
Note

Indexing with s[i] gives you a byte, not a character. for range is the safe way to iterate over characters.

String conversions

FromToCopy?
string[]byteYes (usually)
string[]runeYes
[]bytestringYes (usually)
[]runestringYes

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, encoding
  • regexp — regular expressions (returns []byte or string matches)

Key takeaways

  • Strings are immutable byte slices with explicit length
  • len() gives bytes; use utf8.RuneCountInString() for character count
  • for range iterates runes; s[i] indexes bytes
  • Use strings.Builder for efficient concatenation
  • Slicing shares memory — use strings.Clone() to avoid retaining large strings