String and []byte Internals

Updated

September 8, 2026

String and []byte Internals

Overview

Strings are immutable ptr+len headers over bytes (usually UTF-8). []byte is ptr+len+cap. Converting between them often allocates — a hot-path footgun.

Diagram: Headers

diagram:
  S[string ptr+len] --> Imm[immutable bytes]
  B[slice ptr+len+cap] --> Mut[mutable]
  Conv[string b / byte s] --> Copy[usually copy]

Headers

string:  { data *byte, len int }           // immutable
[]byte:  { data *byte, len int, cap int }  // mutable
s := "go"
b := []byte(s) // copy
s2 := string(b) // copy

Compiler sometimes proves conversions are temporary and elides copies (e.g. map lookup m[string(b)] optimizations in modern Go) — measure, don’t assume.

Range and Indexing

for i, r := range s { /* r is rune; i is byte index */ }
s[i] // byte, not rune

unsafe Conversions (Advanced)

// historical patterns; prefer safer APIs / newer runtime helpers when available
// wrong lifetime → memory corruption

Only with full understanding of immutability: never mutate a []byte view of a string.

Builder and Buffer

  • strings.Builder — string assembly
  • bytes.Buffer — also io.Writer
  • bytealg runtime helpers power strings/bytes (assembly on some arch)

Experiment

go test -bench=. -benchmem
package conv_test

import "testing"

var sink string

func BenchmarkByteToString(b *testing.B) {
    raw := []byte("authorization: bearer ...")
    for i := 0; i < b.N; i++ {
        sink = string(raw)
    }
}

func BenchmarkMapKey(b *testing.B) {
    m := map[string]int{"authorization: bearer ...": 1}
    raw := []byte("authorization: bearer ...")
    for i := 0; i < b.N; i++ {
        _ = m[string(raw)]
    }
}

What to notice: Map key optimization may reduce allocs vs naive assign to sink.

Try next: Profile a JSON logger converting large []byte bodies to string.