skip to content

What conditions would make you allow unsafe.String zero-copy conversions in your team's Go codebase?

level: principalimportance: nice to knowfreq 22%

answer

  1. The technique is trivial; the promise is not
  2. Who is allowed to write those bytes, and when
  3. Tooling checks the conversion, not the later write
  4. Measure first, then contain it to one package
  5. The stdlib pays for it with a stated invariant

basics

~20 s

Allow it only where a profile shows the conversion copy actually costs, where the bytes provably can never be written again, and where the trick stays inside one package behind an ordinary safe API. Everywhere else the standard says no.

solid answer

~50 s

The technique is not the decision; the ownership of the bytes is. A zero-copy `string` over a `[]byte` is sound only if nothing ever writes that array again for as long as the string is reachable, and no compiler check or test can prove that — the race detector's `checkptr` instrumentation catches malformed pointer conversions, not a legitimate-looking write three files away. So I write the standard as: measured evidence first (a benchmark with `-benchmem` and a profile showing the copy matters), then containment — the conversion lives in one unexported helper, in one package, with a doc comment stating the caller's obligation, and it never crosses an exported API where another team could hand it a slice they intend to reuse. Before any of that, the free wins are mandatory: `m[string(b)]`, `strings.Builder`, `append(b, s...)`, `io.WriteString`, and keeping data on the `bytes` side of the fence. I would rather approve one reviewed use in a decoder than a helper the whole company imports.

go deeper

for a junior

You will not set this policy, but know that converting a []byte to a string copies the bytes, that the unsafe package can skip the copy, and that doing so is not something to reach for on your own.

for a middle

Be able to state the obligation precisely: nothing may write the byte array for as long as the string is reachable. Know the safe alternatives that remove the copy without the obligation.

for a senior

Show how you would prove the copy costs before removing it, and how you would contain the conversion — one unexported helper, no exported surface, invariant documented, tests and -race in CI.

for a principal

Own the rule and its enforcement: what evidence is required, who may grant an exception, why a shared helper is worse than a local one, and what triggers revisiting an exception you granted last year.

## Why this is a policy question and not a technique question `unsafe.String(ptr, len)` builds a `string` over bytes you already have, with no allocation and no copy. Mechanically it is one line. What it actually does is move a guarantee the language used to enforce — that a string's bytes never change — onto a human promise. The interesting question for someone who owns a codebase's standards is therefore: under what conditions is that promise cheap enough to keep, and who is on the hook when it is broken? ## The obligation, stated plainly A string built over a byte array is only valid while **nothing writes those bytes for the entire lifetime of the string**. Two failure shapes follow: - **A later write.** The slice is refilled by the next read from an `io.Reader`, or reused for the next record in a decode loop, and strings handed out earlier silently change. Map keys stop matching the entries they were inserted under. Comparisons that succeeded a moment ago fail. Nothing crashes; the data is just wrong, intermittently. - **A leaked reference.** The string is stored in a cache, a struct field or a channel message and outlives the scope the author was reasoning about. Now the constraint applies to code the author never saw. Both are aliasing bugs, and both are invisible at the site of the conversion. ## What tooling actually gives you This is where a standard has to be honest. Building and running the test suite with `-race` enables `checkptr` instrumentation, which catches malformed pointer arithmetic — a converted pointer that straddles two allocations, or one whose alignment is wrong for the target type. It is worth requiring, and it will catch a genuinely broken conversion. It will not catch the failure you actually fear. A perfectly well-formed `unsafe.String` over a slice that somebody later writes to is not a data race and not a malformed conversion; it is a correctness bug the tooling has no opinion about. `go vet` will not flag it either. So the control is review, plus the containment rules below — not a check you can bolt onto CI and forget. ## The conditions I would write down 1. **Measured, not assumed.** A benchmark with `-benchmem` before and after, and a profile identifying the conversion as a real cost in a real workload. "It saves a copy" is not evidence that the copy mattered. 2. **The cheap alternatives are exhausted first.** `m[string(b)]` for map lookups, comparison against a literal, `strings.Builder` for assembly, `append(dst, s...)` and `copy(dst, s)`, `io.WriteString`, and the `bytes` package mirrors so data can stay in one representation end to end. Most proposals die here, which is the point. 3. **The bytes are provably dead to writers.** Ideally the slice was allocated in the same function, fully written, and never referenced again — the shape where a local proof fits on one screen. A slice that will be refilled by the next call is disqualified outright. 4. **Containment.** The conversion lives in one small unexported helper in one package. It does not appear in an exported signature, and it is not published as a general-purpose utility other teams import — a shared helper hands the sharp edge to callers who never read the reasoning. 5. **Documentation at the site.** A comment stating the invariant, why it holds here, and what would break it, so the next reader can re-derive the argument instead of trusting it. 6. **Tests that pin the invariant.** A test that mutates the source slice after conversion and asserts the code path no longer depends on the string, or at minimum a test that fails loudly if the buffer is reused, plus the whole suite under `-race`. If a proposal cannot meet all six, the answer is no, and the reviewer does not need to argue about nanoseconds. ## The organisational half A standard that says "never use unsafe" gets ignored by the one team that genuinely needs it, and then the use is unreviewed. A standard that says "use it when it helps" spreads it everywhere. The workable version names an owner: the conversion is allowed in listed packages, each with a named maintainer who is expected to re-justify it when the surrounding code changes. That also gives you a deletion trigger — when the hot path is rewritten or the workload moves, the exception is re-argued rather than inherited. It is worth pointing at how the standard library itself buys the same trick: `strings.Builder` returns a zero-copy string, but only because it holds an invariant it can state in one sentence — it only ever appends — and it backs it with a runtime check that panics if the Builder is copied. That is the bar. If a proposed use cannot state its invariant in one sentence and cannot fail loudly when the invariant is broken, it is not equivalent, and it should not be treated as though it were. ## What a strong answer sounds like It starts from ownership of the bytes rather than from performance, it is explicit that tooling does not cover the real risk, it names the free alternatives first, and it ends with a rule someone can apply in a code review without re-litigating the whole argument each time.

  • An engineer says the race detector will catch it if anyone writes to the slice later. Is that true?
    No. A `-race` build enables checkptr, which catches malformed conversions such as a pointer straddling allocations or a misaligned target, and it catches concurrent unsynchronised access. A single-goroutine write to a slice you aliased as a string is neither — it is an ordinary, correctly synchronised write that happens to be wrong. Review is the control.
  • Would you publish an internal `BytesToString` helper so teams stop hand-rolling it?
    No. A shared helper hands the obligation to callers who never read the argument for why it is safe, and its usage grows fastest in exactly the code least likely to have been profiled. Keep the conversion unexported, at the site that proved it needed it, with the reasoning attached.
  • What would make you revoke an existing exception?
    The workload changed and the profile no longer shows the copy mattering; the surrounding code grew a path that stores or returns the string; the buffer became shared or reused; or the owning maintainer left. Each exception should carry a named owner and a re-justification trigger so it can be deleted rather than inherited.

saying these in an interview costs you the question

  • Treats it as a routine optimisation needing no justification
  • Claims a -race build proves the conversion is safe
  • Exports a general-purpose zero-copy helper for all teams
  • Skips the free wins like m[string(b)] and strings.Builder
  • Cannot state the invariant the string depends on
  • Justifies it on a microbenchmark with no production profile