skip to content

As Go platform owner, how do you decide which packages may import unsafe, and what do you demand for an exception?

level: principalimportance: nice to knowfreq 25%

answer

  1. default deny, enforced by the build
  2. a list of paths, each with a person
  3. measure the safe version first
  4. one small package, not sprinkled
  5. the compatibility promise stops at this import

basics

~20 s

Default to a ban enforced in CI, with a short allowlist of package paths each having a named owner. Grant exceptions only against a benchmark the safe version cannot match, with conversions confined to one small package.

solid answer

~50 s

Make the default deny and enforce it mechanically — an import check in CI over the whole module, not a review habit — because these conversion rules cannot be checked by eye at review speed. Keep an allowlist of specific package paths, each with a named owner rather than a team, and grant entries against evidence: a benchmark showing the safe implementation is measurably too slow at real scale, the conversions confined behind a narrow API in one package, each commented with the documented `unsafe` pattern it relies on, and that package's tests running under `-race` so checkptr validates them. Two costs belong in the decision explicitly. Packages that use `unsafe` are outside the Go 1 compatibility promise, so whoever owns the allowlist entry also owns the toolchain-upgrade risk. And an exported API that hands raw pointers to other teams exports the lifetime rules with it — that one I refuse regardless of the benchmark.

go deeper

for a junior

You will not set this policy, but know that many Go codebases restrict the unsafe import and that adding one is a conversation to have before writing the code, not after.

for a middle

Be able to make the technical case in either direction: what the safe implementation costs in allocations and time, and what specifically becomes unverifiable once conversions are in the diff.

for a senior

Show what you would require of a package you were asked to approve — containment behind a small API, commented patterns, tests under -race, and a layout test — and what you would send back.

for a principal

Own the whole mechanism: default deny enforced by the build, an allowlist of paths with named owners and review dates, evidence rules for exceptions, and an explicit account of who inherits the compatibility risk at the next toolchain upgrade.

## Why this is a policy question at all Every other rule in this area is a fact about the language. This one is not: `unsafe` is a legitimate tool, the standard library uses it, and the fastest implementations of some real problems need it. The decision is where the authority sits and what evidence moves it — which is a call somebody owns and can be overruled on. The reason it cannot be left to individual review judgment is specific. The rules for `unsafe.Pointer` conversions are subtle, mostly invisible in a diff, and failures do not surface at the site of the mistake: a stored `uintptr` produces a use-after-free somewhere else, minutes later, under load. A reviewer who does not already have those rules in their head will approve the code, and the reviewer who does have them cannot be in every review. ## The default and the mechanism Default deny, enforced by a check in CI that fails a build when a package outside the allowlist imports `unsafe`. Mechanical enforcement matters more than the list's contents: it turns a judgment made under time pressure into a conversation that happens before the code is written. The allowlist should name package paths, not directories or teams, and each entry should carry an owner's name, the reason, and a review date. A list with entries whose rationale nobody can reconstruct is worse than no list, because it launders the decision. ## What an exception has to show - **A benchmark, not an intuition.** The safe implementation must exist and must be measurably too slow at production scale, with allocation counts reported. Most requests die here honestly: someone tried the conversion first and never wrote the boring version. - **Containment.** All conversions live behind a small API in one package — a few accessor functions — rather than sprinkled through call sites. Containment is what makes the remaining review tractable and what makes removal possible later. - **Documented patterns cited.** Each conversion carries a comment naming which of the `unsafe` package's documented valid patterns it is. If the author cannot name one, that is the answer. - **Instrumented tests.** That package's tests run with `-race` in CI so the checkptr instrumentation validates conversions, plus `go vet` for its `unsafeptr` check, plus a test asserting any layout assumptions the code makes. - **An exit condition.** What would make this unnecessary — a compiler improvement, a standard-library addition, a change in the data volume. Without it, the entry is permanent by default. ## The two costs that are usually left out of the argument **Compatibility.** Ordinary Go code is covered by the Go 1 compatibility promise. Code that depends on the internal properties `unsafe` exposes is explicitly not. That is not theoretical: it means the person upgrading the toolchain inherits work from the person who wrote the fast path, possibly years later. The allowlist entry should make that ownership explicit, and the owner should be someone still accountable for the package. **Blast radius across an API boundary.** A package that uses `unsafe` internally and returns ordinary values contains its own risk. A package that returns pointers into a mapped region, or hands out values whose validity depends on a mapping staying open, has exported the lifetime rules to callers who cannot see them and will not read the doc comment. That is the case I refuse even with a good benchmark: change the API to return copies, bounds-checked offsets, or a handle with an explicit `Close`, and then re-argue the performance case against that shape. ## What to do with the code that already exists An inherited codebase will have `unsafe` in places nobody remembers. Inventory it, then triage in this order: conversions in exported APIs first, because their risk crosses a boundary; then anything storing a `uintptr` in a variable or field, which is the shape most likely to be outright wrong; then everything else. Delete what has no benchmark behind it — a safe rewrite that costs a few percent on a path nobody profiles is a trade worth taking every time. Grandfather the rest onto the allowlist with an owner and a date rather than pretending the ban applied retroactively. ## How you know the policy is working The allowlist gets shorter over time, exceptions arrive with benchmarks attached because people know they will be asked, and toolchain upgrades stop producing surprises from those packages. If instead the list grows and every entry's justification is that the safe version was never written, the policy is theatre and the honest move is to say so and re-decide.

  • Which unsafe exception would you refuse even with a convincing benchmark?
    One that exports the risk. If a package returns pointers into a mapped region, or values whose validity depends on a mapping staying open, every caller now owns a lifetime rule they cannot see and will not read. Change the API to return copies, offsets, or a handle with an explicit Close, then re-argue the numbers against that shape.
  • Why does importing unsafe change who owns toolchain upgrades?
    Because the Go 1 compatibility promise does not cover code depending on the internal properties unsafe exposes. Ordinary code keeps working across releases; a package that reinterprets memory can break. Naming an owner on each allowlist entry is how that future work gets assigned before it lands on whoever happens to run the upgrade.
  • How do you handle unsafe that is already in an inherited codebase?
    Inventory it, then triage: conversions in exported APIs first, then anything storing a uintptr in a variable or field, then the rest. Delete whatever has no benchmark behind it, and grandfather the remainder onto the allowlist with an owner and a review date rather than pretending the new ban was retroactive.
  • What signal tells you the policy has become theatre?
    The allowlist grows, and the justification on new entries is that nobody wrote the safe version to compare against. A working policy produces a shrinking list, exceptions that arrive with numbers attached, and toolchain upgrades that stop springing surprises from those packages.

saying these in an interview costs you the question

  • Bans unsafe outright with no exception path at all
  • Leaves the decision to individual reviewer judgment
  • Accepts an exception on an intuition about speed
  • Ignores that unsafe forfeits the compatibility promise
  • Allows raw pointers into mapped memory across an exported API