debug a problem involving String Immutability
Fast recall
Quick revision
Core answer
Practical understanding of String Immutability.
Repeating a definition without saying when to use it.
Give one trade-off and say how you would test it.
Interview-ready guide
Interview answer
There's no in-place edit — the path is always convert → mutate → convert:
s := "hello"
b := []byte(s) // copy to mutable bytes
b[0] = 'H'
s2 := string(b) // copy back — "Hello" (s unchanged)
r := []rune(s) // copy to mutable runes
r[0] = 'H'
s3 := string(r) // "Hello" — rune-safe for multibyteExecuted earlier: the original stays "hello" every time.
The []byte vs []rune choice: byte for ASCII-safe/binary handling (faster, one byte per element); rune when changing CHARACTER content (each element is a code point — a multibyte char replaces as one element, never corrupts).
The functional alternative — when the change is a transform, use the transform functions (which allocate internally and return new strings):
strings.ToUpper(s)
strings.Replace(s, "l", "L", 1)
strings.Map(func(r rune) rune { if r == 'l' { return 'L' }; return r }, s)strings.Map's subtlety: it returns the ORIGINAL string (zero alloc) when the function changes nothing — the runtime checks for identity. That's the closest Go gets to in-place: avoiding the copy when no change happened.
debug a problem involving String Immutability
Direct Answer
Convert to a mutable type ([]byte or []rune), change, convert back — each conversion copies. Or use strings.Replace/Map which return NEW strings. You never write through a string.
Deep Dive & Architecture
Why the round-trip is two copies and why that's the honest cost: []byte(s) copies bytes into a mutable buffer (the string can't hand out writable access to read-only memory); string(b) copies into a fresh immutable allocation (the buffer could change again — the conversion must snapshot). Immutability's price is paid at each boundary crossing, and that's WHY the API has no 'modify in place' — no such state exists on the string side.
The rune conversion's extra work: []rune(s) DECODES UTF-8 to code points (O(n) with per-rune decoding work), string(r) RE-ENCODES. The byte round-trip is two memcpys; the rune round-trip is decode + encode. ASCII input makes them nearly identical; heavy multibyte content widens the gap. Choosing []rune is correctness (character-level edits), not speed.
strings.Map's zero-copy fast path, mechanically: it writes output to a buffer lazily — the first changed rune triggers buffer allocation and backfill of prior runes; if the map function returns every input rune unchanged, the original pointer is returned untouched. For 'usually no changes' workloads (validation that occasionally rewrites), that's a free win — and a behavior worth knowing because callers can compare pointer identity (unsafe but real) to detect the no-change case.
The full toolkit by job: one substring change → strings.Replace; systematic transform → strings.Map or ToUpper; many edits → []rune once, edit the slice, string(r) once (not repeated conversions — the batch-then-convert pattern, the same amortization lesson as Builder); binary data → []byte the whole way (it was never text).
Common Mistakes & Pitfalls
Repeating a definition without saying when to use it.
Check your understanding
Answer these without looking back at the article.
- What is String Immutability, and when would you use it?
- When would you use String Immutability in a real project?
- How would you compare String Immutability with an alternative?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.