Why does runtime.KeepAlive exist, and what bug does it prevent in a wrapper holding a descriptor?
answer
- dead at last use, not at return
- the descriptor outlived its wrapper
- the collector cannot see your syscall
- one call the compiler must not remove
- put it after the last use
basics
~20 sruntime.KeepAlive marks a value as still in use at that line. Without it, an object can be collected as soon as its last field read, letting a registered cleanup close the descriptor while a call still uses it.
solid answer
~50 sGo's collector works from reachability as the compiler computes it, not from lexical scope: a local variable is dead after its last use, which can be many instructions before the function returns. So in a method that loads `f.fd` and then makes a long call with that integer, `f` is already unreachable while the call is in flight, and a `runtime.AddCleanup` or finalizer registered on `f` may run and close the descriptor underneath it. The best case is a spurious error; the worst is that the descriptor number gets reused by another goroutine and you write into somebody else's file. `runtime.KeepAlive(f)` is a call the compiler treats as a use of `f`, so `f` stays reachable up to that line; place it after the last use of the extracted value. Assigning `_ = f` is not a substitute. It is only needed when something is registered on the wrapper.
code
go · 5 linesfunc (b *buf) write(p []byte) error {
_, err := syscall.Write(b.fd, p) // b is dead once b.fd has been loaded
runtime.KeepAlive(b) // ...so keep it reachable until the write returns
return err
}go deeper
Know that runtime.KeepAlive exists and that its job is to say a value is still in use at that line, so the collector does not treat it as garbage any earlier.
Explain the liveness rule behind it: a variable dies at its last use, which may be well before the function returns, so a wrapper can be collected while a descriptor copied out of it is still being used.
Be able to spot it in review — any type with a registered cleanup that hands a raw handle to an outside call — and to describe the production symptom of getting it wrong, including descriptor reuse.
Judge whether a design that needs this is worth keeping. An API that never exposes a raw handle, or that makes an explicit Close the only release path, removes the whole class of bug instead of relying on every author remembering a keep-alive.
## Liveness is not scope Programmers coming from languages with scope-based destruction read `func (f *file) write(...)` and assume `f` is alive until the closing brace. Go makes no such promise. The compiler computes *liveness*: a variable keeps its object reachable from its definition to its last use, and after that use the object is, as far as the collector is concerned, garbage — even though the function has not returned and the variable is still in scope. That is normally invisible, and normally a good thing: it lets the collector reclaim large intermediate values before a long function ends. It becomes visible the moment something else is watching the object's death. ## The bug Take a small wrapper around an operating-system resource: a struct holding an integer descriptor, with a `runtime.AddCleanup` (or, in older code, a `runtime.SetFinalizer`) registered on the wrapper so a forgotten `Close` does not leak the descriptor. Now write the obvious method: ``` func (b *buf) write(p []byte) error { _, err := syscall.Write(b.fd, p) return err } ``` The field read `b.fd` loads an integer. After that load, nothing in the function uses `b` again. `b` is dead. If the caller also dropped its last reference, the wrapper is unreachable while `syscall.Write` is still running, a collection may find it, and the cleanup may close the descriptor mid-write. The symptoms are nasty precisely because they are rare and load-dependent. The mild version is a "bad file descriptor" error that nobody can reproduce. The severe version is descriptor reuse: the kernel hands the freed number to another goroutine's newly opened file, and the write in flight lands on the wrong file. Nothing in the code that suffers the corruption is wrong. ## What KeepAlive does `runtime.KeepAlive(x)` does nothing at runtime. Its entire contract is toward the compiler: it counts as a use of `x` at that point in the program, so liveness analysis keeps `x` reachable up to that line, and the compiler is not permitted to move or elide it. Place it *after* the last use of the extracted handle — after the call, not before it: ``` func (b *buf) write(p []byte) error { _, err := syscall.Write(b.fd, p) runtime.KeepAlive(b) // b must outlive the write, not just the field load return err } ``` Substitutes do not work. `_ = b` is an assignment to the blank identifier that the compiler may see straight through, and it expresses no ordering. A comment obviously does nothing. `runtime.KeepAlive` exists because the language needed one supported way to say "this value is still in use here". ## When you need it, and when you do not You need it only when something is registered on the object whose early death is observable: a cleanup, a finalizer, or an ownership relationship where another party frees a resource once the Go object dies. The classic cases are wrappers around file descriptors, handles obtained from `cgo`, and memory allocated outside the Go heap whose lifetime is tied to a Go object. You do not need it for ordinary data. If the object has no hook attached, nothing observes its collection, and early reclamation is exactly the optimisation you want. ## The design that removes the need `runtime.KeepAlive` is a patch on an API shape. Every method that hands a raw handle to an outside call is one review away from missing it, and the mistake produces a bug that will not reproduce. The alternatives are structural: do not expose or extract the raw handle at all; keep the resource behind methods that use the wrapper itself for the whole call; or drop the runtime hook and make the explicit `Close` the only release path, at which point there is nothing to fire early. Reaching for a keep-alive in review is a fine fix, but noticing why the type needs one is the better observation.
- Does runtime.KeepAlive force the registered cleanup to run when it returns?No. It has no runtime effect beyond keeping the value live at that point, and it does not schedule anything. After that line the object becomes collectable as usual, and the cleanup may run at some later moment, or never — the guarantees are unchanged.
- Would writing `_ = b` at the end of the method do the same job?No. The compiler may see straight through an assignment to the blank identifier, and it expresses nothing about ordering relative to the call you are protecting. `runtime.KeepAlive` exists precisely because it is recognised as a use of its argument at that program point; it is the only supported way to say it.
- How does the missing keep-alive present in production?As a rare, load-dependent failure: unexplained "bad file descriptor" errors, or, worse, a write landing on an unrelated file after the descriptor number was reused. It gets more likely under memory pressure, when collections are frequent, and it usually resists reproduction, so it is normally found by reading the code rather than by debugging it.
Handing someone your locker number and walking away: the attendant is free to clear the locker the moment you leave, even though the number in the other person's hand still looks perfectly valid.
saying these in an interview costs you the question
- Thinks a local stays live until the function returns
- Uses a blank assignment instead of runtime.KeepAlive
- Believes KeepAlive forces the cleanup to run
- Places the keep-alive before the last use of the handle
- Assumes the race cannot happen because the method is short