skip to content

What does unsafe.String(unsafe.SliceData(b), len(b)) return for a []byte b, and what must you never do to b afterwards?

level: juniorimportance: should knowfreq 25%

answer

  1. no allocation at all
  2. two views onto one byte array
  3. a pointer plus a length, wrapped
  4. you inherit the immutability promise

basics

~20 s

It returns a string that reuses b's existing byte array instead of copying it, so the conversion allocates nothing. Because Go strings must never change, nothing may write to b again for as long as that string is alive.

solid answer

~50 s

`unsafe.SliceData(b)` hands you the `*byte` at the start of b's backing array, and `unsafe.String(ptr, n)` wraps that pointer plus a length into a string value. Together they build a string that points straight at b's bytes: no allocation, no copy, constant time whether b holds eight bytes or eight megabytes. The ordinary `string(b)` copies precisely so the result can honour Go's guarantee that a string never changes; the unsafe form skips the copy and moves that guarantee onto you. From then on the slice and the string are two views of one array, so any write through b mutates a value the language, compiler and runtime all assume is frozen. The pattern is only sound when the `[]byte` is genuinely dead after the conversion. At run time it panics if the length is negative, or if the pointer is nil and the length is not zero.

code

go · 9 lines
go
b := []byte("hello")

// []byte -> string, no copy: s borrows b's array
s := unsafe.String(unsafe.SliceData(b), len(b))

// string -> []byte, no copy: len and cap are both len(s)
back := unsafe.Slice(unsafe.StringData(s), len(s))

// From here on, writing to b or to back mutates s. Don't.

go deeper

for a junior

Be ready to state in one line what it does: it makes a string that shares the slice's bytes instead of copying them. Add the condition immediately — the slice must be treated as finished.

for a middle

Explain the mechanics. SliceData yields the pointer, String wraps pointer plus length into a string, the length must come from len not cap, and the call panics on a negative length or a nil pointer with a non-zero length.

for a senior

Show where you would allow it in review: a buffer built in one place and handed over once. Show where you would reject it: anything derived from a reader, a pool or a decoder that reuses its storage.

for a principal

Own the API consequence. A function that returns a borrowed string pushes a lifetime rule onto every caller forever, so decide whether the saved copy justifies documenting and policing that rule across teams.

## The two headers involved A Go `string` value is a small header: a pointer to some bytes and a length. A `[]byte` is a slightly larger header: a pointer, a length and a capacity. The bytes themselves live in an array somewhere else in memory. Because the two headers are so close in shape, converting between them is *almost* free — except that the ordinary conversion `string(b)` deliberately is not free: it allocates a fresh array, copies every byte of `b` into it, and points the new string at that private copy. That copy is not an oversight. Go's specification says a string is immutable, and `b` is a mutable slice that its owner can write to at any moment. The only way the language can promise the string will never change is to give the string bytes nobody else can reach. ## What the two unsafe calls do `unsafe.SliceData(b)` returns a `*byte`: the address of the first element of the array that backs `b`. `unsafe.String(ptr, n)` takes a `*byte` and an integer length and produces a `string` whose bytes begin at `ptr` and whose length is `n`. Composed as `unsafe.String(unsafe.SliceData(b), len(b))`, they build a string header that points directly at `b`'s existing array. There is no allocation and no `memmove`; the cost is a couple of instructions regardless of how large `b` is. That is the entire attraction: on a hot path that turns millions of freshly read byte slices into strings, the copy can dominate both CPU time and garbage-collector pressure. Note that `len(b)` is the right length, not `cap(b)`. The capacity may extend past the bytes you actually filled, and a string built over the capacity would expose whatever stale data sits in the spare room. ## The promise you sign What you have skipped is not the copy so much as the *isolation* the copy bought. After the conversion, the slice and the string alias one array. Writing `b[0] = 'X'` changes the string. That is not merely surprising — it violates a rule the whole toolchain leans on, so the consequences are not confined to the value you touched. A string already used as a map key was hashed at insert time and will not be rehashed. Comparisons the compiler has already proven can be reused. Every copy of the string, and every substring taken from it, shares the same array and sees the mutation. So the discipline is a lifetime rule: the `[]byte` must be dead the moment the conversion happens. The clean case is a buffer you built yourself, in one place, and hand over exactly once — an encoder that assembles a payload and returns it, a `strconv`-style routine that formats digits into scratch space and yields the result. The dangerous case is any slice you did not allocate: bytes handed to you by a reader, a scanner, a pool, or a decoder that will refill the same array for the next item. ## Runtime rules and edge cases `unsafe.String` panics at run time if the length is negative, or if the pointer is nil while the length is non-zero. A nil pointer with a zero length is explicitly allowed and yields the empty string. Nothing checks that the length actually fits the allocation the pointer came from — an over-long length silently reads past the end of the array, and that is on you. On the other side, `unsafe.SliceData` returns nil for a nil slice, and for a non-nil slice with zero capacity it returns a non-nil pointer to an unspecified address. Pairing that with `len(b)`, which is then zero, keeps you inside the rules. The resulting string is a completely ordinary Go string as far as the garbage collector is concerned: its data pointer is a real pointer, so the backing array stays alive while the string is reachable. Zero-copy does not mean the memory can be collected out from under you; it means only that mutation is now your problem. ## The reverse direction The mirror pair is `unsafe.Slice(unsafe.StringData(s), len(s))`, which produces a `[]byte` of length and capacity `len(s)` aliasing the string's bytes. It is subject to the same rule from the other end: the resulting slice must be treated as read-only, because writing to it mutates a string. Since its capacity equals its length, an `append` reallocates rather than overwriting — but a direct element assignment does not. ## How to review it When you see this pattern in a diff, ask one question: who else can reach these bytes, and for how long? If the answer is "nobody, the slice never escapes this function", the conversion is fine and deserves a comment saying so. If the answer involves a buffer that will be reused, or a slice that was passed in from outside, ask for the plain `string(b)` and let the benchmark decide whether the copy is worth removing at all.

  • What happens if you call unsafe.String with a nil pointer and a length of zero?
    That combination is explicitly allowed and yields the empty string. The run-time panic fires only when the length is negative, or when the pointer is nil and the length is non-zero. It pairs neatly with `unsafe.SliceData`, which returns nil for a nil slice and a non-nil pointer to an unspecified address for an empty non-nil one — passing `len(b)` alongside it keeps you legal in both cases.
  • Does the string returned by unsafe.String keep the backing array alive for the garbage collector?
    Yes. The result is an ordinary string value whose data pointer is a real Go pointer, so the collector keeps the array alive as long as the string is reachable. What zero-copy removes is the isolation, not the reachability: the array will not disappear under you, but anyone still holding the slice can rewrite it.
  • Why is len(b) the correct length rather than cap(b)?
    The capacity can be larger than the part of the array you actually filled — the spare room holds stale bytes from a previous use, or nothing meaningful at all. Building the string over `cap(b)` would expose that data as if it were content, so the length always comes from `len(b)`.

The ordinary conversion posts someone a photocopy. The zero-copy conversion lends them your only original and promises you will never write on it again.

saying these in an interview costs you the question

  • Says unsafe.String copies the bytes, only faster
  • Thinks writing to the slice afterwards is fine if nothing reads it yet
  • Applies it to a buffer that will be refilled later
  • Passes cap(b) instead of len(b) as the length
  • Expects the compiler to reject an illegal write to the shared array