skip to content

Why can't you assign to s[0] on a Go string, and what do you do instead?

level: middleimportance: should knowfreq 56%

answer

  1. the value cannot be written to
  2. the compiler stops you, not the runtime
  3. build a new value instead
  4. convert out, edit, convert back
  5. byte position is not character position

basics

~20 s

Go strings are immutable, so s[0] = 'G' is a compile error. Build a new value: convert to []byte, or to []rune when the character is not ASCII, edit it, and convert back. Both conversions copy.

solid answer

~40 s

A string value in Go is immutable, so `s[0] = 'G'` does not compile — the compiler rejects it with "cannot assign to s[0]", and you cannot take its address either. Immutability is what lets strings be shared and copied freely, used as map keys, and sliced into substrings without anyone worrying that the text will change underneath them. To modify text you build a new value: `b := []byte(s)`, write into `b`, then `s = string(b)`. Both conversions copy the bytes, so a round trip costs two allocations. Use `[]byte` only when you are editing ASCII, because byte positions are not character positions; to change an accented or CJK character, convert with `[]rune(s)`, edit the code point, and convert back with `string(r)`.

code

go · 7 lines
go
s := "go"
// s[0] = 'G' // compile error: cannot assign to s[0]

b := []byte(s) // copy out
b[0] = 'G'
s = string(b) // copy back
fmt.Println(s) // prints: Go

go deeper

for a junior

Know that a Go string cannot be changed in place and that the compiler rejects the assignment outright. Remember the fix as a round trip: convert to a slice, edit it, convert back.

for a middle

Explain why the conversions copy and what immutability buys — free sharing, cheap substrings, safe map keys. Be able to justify choosing []rune over []byte when the edit is positioned by character.

for a senior

Show you think about the allocation cost: convert once in and once out rather than per step, and reach for an accumulating builder instead of repeated concatenation when assembling text in a loop.

for a principal

Treat the byte-versus-character choice as an API decision. An interviewer expects you to argue for exposing text as strings at package boundaries and keeping mutable slice forms private to the routine that needs them.

### The rule A Go string is an **immutable** sequence of bytes. Once a string value exists, nothing in the language can change its contents. `s[0] = 'G'` is not a runtime failure or an undefined behaviour — it is a compile error, because `s[0]` is a value, not a storage location. For the same reason `&s[0]` does not compile: there is nothing addressable to point at. This catches out anyone arriving from a language where a string, or its byte buffer, can be poked in place. ### Why the language works this way Immutability buys several properties that Go leans on heavily: - **Free sharing.** Assigning a string, passing it to a function, or storing it in a struct never needs a defensive copy, because no holder can modify it. - **Cheap substrings.** `s[a:b]` is a substring expression that does not need to copy the text, precisely because neither the substring nor the original can be written to. - **Safe map keys.** A string can be a map key; a mutable key would corrupt the map the moment someone changed it after insertion. - **Predictable concurrency.** A string handed to another goroutine cannot be mutated out from under it, which removes an entire class of data race. The cost is that every edit produces a new value. ### How to change text **Editing ASCII — go through a byte slice.** ``` b := []byte(s) b[0] = 'G' s = string(b) ``` The conversion `[]byte(s)` produces a **new, mutable slice holding a copy** of the bytes; `string(b)` copies them back into a new immutable string. The round trip therefore costs two copies, which matters in a hot loop and is invisible in ordinary code. **Editing non-ASCII — go through a rune slice.** ``` r := []rune(s) r[3] = 'e' s = string(r) ``` `[]rune(s)` decodes the UTF-8 into a slice of code points, so index 3 means "the fourth character", not "the fourth byte". Converting back re-encodes. This is the conversion to reach for whenever the position you care about is a character position — with a byte slice, writing at index 3 of `"José"` would overwrite half of the accented letter and leave the string invalid. Note also that the replacement character need not have the same encoded width as the one it replaces, so the new string can be shorter or longer in bytes than the old one. That is another reason the operation has to build a new value rather than patch the existing one. **Building rather than editing.** Most "modify a string" tasks are really "produce a new string": concatenation, `strings.Replace`-style substitution, or accumulating pieces. Repeated `s += piece` in a loop allocates a fresh string on every iteration; `strings.Builder` exists to accumulate text without that cost. ### Immutability is not deep for byte slices The symmetry is worth stating: a `[]byte` **is** mutable, and converting from one to the other copies in both directions specifically so that mutating the slice cannot be observed through the string. If conversions shared memory, writing to the slice would silently change a value that other code believes is frozen — which is exactly the guarantee the language is protecting. ### What an interviewer is checking Three things, usually: that you know the assignment is rejected at compile time rather than tolerated; that you can name the byte-slice round trip and its cost; and that you pick `[]rune` rather than `[]byte` when the edit is positioned by character. The third is the one that separates someone who has only handled ASCII from someone who has shipped text handling. ### A practical note If you find yourself doing byte-slice round trips repeatedly on the same value — building a result character by character, say — the answer is usually to keep the working form as a slice for the whole operation and convert to a string once at the end, rather than converting on every step. One conversion in and one out is the shape to aim for.

  • Why do the []byte and string conversions copy rather than share the same memory?
    Because sharing would break immutability. If `[]byte(s)` handed back the string's own storage, writing to the slice would change a value that other holders — including map keys and other goroutines — believe is frozen. Copying in both directions is what makes the guarantee real, and it is why the round trip costs two allocations.
  • When should you use []rune rather than []byte for the edit?
    Whenever the position you care about is a character position, or the character you are writing is not ASCII. Byte index 3 of "José" is the first half of the accented letter, so writing there corrupts it; rune index 3 is the accented letter itself. Use `[]byte` for ASCII-positioned work, `[]rune` when characters matter.
  • What does immutability let the language do that a mutable string could not?
    Share freely without defensive copies, take substrings with `s[a:b]` without copying the text, use strings as map keys safely, and hand a string to another goroutine with no risk of a data race on its contents. All four rest on nobody being able to write into it.

A string is a printed page, not a whiteboard: to change a word you photocopy it, edit the copy, and print a new page.

saying these in an interview costs you the question

  • Thinks s[0] = 'G' compiles and panics at runtime
  • Believes []byte(s) shares memory with the string
  • Edits a multi-byte character through a byte index
  • Says strings are immutable only by convention
  • Builds long text with += in a hot loop