String Immutability
Fast recall
Quick revision
Core answer
Whether you understand String Immutability beyond memorized syntax.
Giving a definition without explaining a practical trade-off.
Connect the concept to a small production example and state its complexity or failure mode.
Interview-ready guide
Interview answer
A Go string is a read-only view over bytes, and the language enforces it: s[0] = 'H' fails to compile. What you CAN do: read bytes (s[0] is a byte value), slice substrings (both share memory — no copy), build NEW strings.
s := "hello"
// s[0] = 'H' // compile error
b := []byte(s)
b[0] = 'H'
fmt.Println(string(b), s) // Hello hello — original untouchedExecuted. Converting to []byte, mutating the copy, converting back — the original is untouched, because string(b) allocates a new string.
Why the language chose immutability: a string value is two words — pointer + length, no capacity, no writes. If strings were mutable, every assignment would either copy bytes (expensive) or share writable memory (one write corrupts another's view). Immutable bytes make the two-word header completely safe to copy — assignment is copying pointer+len, which is why strings are so cheap to pass.
The consequences: comparable with ==, hashable (map keys — bytes can't change so the hash can't rot), safe across goroutines (read-only can't race), O(1) slicing (shares memory).
String Immutability
Direct Answer
Remember: strings are read-only byte views — s[0] = 'H' won't compile. Change requires building a new string. Makes them safe to share, copy, hash (map keys), and pass across goroutines.
Deep Dive & Architecture
The runtime representation: a string is (data-pointer, length) — two machine words, no capacity field, never written through the string type. Every mutating-looking operation — ToUpper, string(b), Replace — allocates a new string; the original's bytes are untouched, because no path exists to write through them.
Why []byte is the mutable counterpart: a []byte has the three-word header (pointer, length, CAPACITY) — the capacity field enables append, and the data is writable. Converting string(b) copies bytes into a fresh immutable allocation; []byte(s) copies into a fresh mutable one. Both directions COPY — the API's honest signal that you crossed a mutability boundary.
The performance folklore: converting string-back-and-forth in a LOOP is the classic Go allocation hotspot. The idioms: strings.Builder for building (mutable buffer, one String() at the end), strings.Map for transforms (returns the ORIGINAL when nothing changed — the runtime checks), bytes.Buffer when the data was always bytes.
The substring-sharing subtlety: s[1:3] shares s's underlying bytes — O(1), no copy. A tiny substring of a huge string keeps the WHOLE huge backing alive (the substring's pointer anchors the parent's start). That's why long-running services use strings.Clone (Go 1.18+) when storing small pieces of large parsed inputs — it breaks the anchor.
Common Mistakes & Pitfalls
Giving a definition without explaining a practical trade-off.
Check your understanding
Answer these without looking back at the article.
- When would you use String Immutability in a real project?
- How would you compare String Immutability with an alternative?
- What is a common mistake when using String Immutability?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.