Between cgo calls, when do you use cgo.Handle versus runtime.Pinner for a Go value C must keep?
answer
- Does C read it, or just carry it?
- An integer is not a pointer
- One keeps it alive, one keeps it still
- Both need an explicit release
- Unpin frees every pin at once
basics
~20 sUse runtime/cgo.Handle when C only needs an opaque token to hand back to Go: it passes an integer, so no pointer crosses at all. Use runtime.Pinner when C must dereference the Go memory itself, and always scope the Unpin.
solid answer
~50 sThe question to ask is whether C needs to *read* the Go memory or merely *carry* a reference back to you. If it only carries it — the classic callback context — use `cgo.NewHandle(v)`, which returns an integer `Handle` you pass as a `C.uintptr_t`; inside the callback `cgo.Handle(token).Value()` recovers the original value. No pointer ever crosses, so any Go value works, closures and maps included, and the handle keeps it alive. You must call `Delete`, or it leaks for the process's life. If C genuinely dereferences the memory — writing into your buffer, walking your struct — the token is useless and you need `runtime.Pinner`: `pinner.Pin(obj)` keeps that object alive and in place so C may legally retain a real pointer, and `defer pinner.Unpin()` bounds it. Handle is the cheaper and safer default; Pinner is what you use when nothing else will do.
code
go · 6 linesh := cgo.NewHandle(sink) // sink may be any Go value, pointers and all
defer h.Delete() // nothing else will ever release it
// hand C.uintptr_t(h) to the codec as its opaque user-data slot.
// In the exported Go callback, recover it:
// sink := cgo.Handle(token).Value().(io.Writer)go deeper
Know that C is not allowed to keep a Go pointer after a call returns, and that the runtime offers two sanctioned ways around it. Being able to name cgo.NewHandle as the token approach is enough at this level.
Explain the mechanics: a Handle is an integer index into a runtime table that also keeps the value alive and must be deleted, while a Pinner stops the collector moving or freeing an object until Unpin. Say which one lets C dereference the memory.
Show the decision rule and its failure modes: choose by whether C dereferences or merely carries, defer every release including on panic paths, and be able to describe what a post-Unpin stale pointer does to a production process.
Own the boundary design. Decide whether Go memory should cross at all, or whether the C side allocates its own storage, and make the C library's retention contract something the code states rather than something a future reader has to infer.
## Two different problems that look alike cgo's rules say C may not retain a Go pointer past the call. Real C APIs want exactly that: `codec_set_callback(fn, ctx)` stores `ctx` and hands it back later. Go offers two escapes, and choosing between them is a design decision, not a preference. ## runtime/cgo.Handle: pass an integer, not a pointer `cgo.NewHandle(v any) Handle` stores the value in a runtime-side table and returns a `Handle`, whose underlying type is `uintptr`. You convert it to `C.uintptr_t` and hand that to C. Because an integer is not a pointer, none of the pointer-passing rules apply — C can store it in a struct, a global, a linked list, for as long as it likes. When C calls back into Go with the token, the exported Go function reconstructs the value: ```go sink := cgo.Handle(token).Value().(io.Writer) ``` Three properties matter: - **Any value works.** A closure, a map, an interface, a struct full of pointers — the value never leaves Go, so its shape is irrelevant. This is the decisive advantage over pinning. - **The handle keeps the value alive.** The runtime table is a live reference, so the collector will not reclaim the value while a handle exists. - **You must `Delete`.** `h.Delete()` removes the entry. Nothing else ever will; a handle per request without a `Delete` is an unbounded leak, and after `Delete`, calling `Value()` on that handle panics. Each `NewHandle` call returns a distinct handle even for the same value, so handles are not deduplicated and each needs its own `Delete`. The cost is one map-ish lookup per conversion, which is nothing against the cost of a cgo call itself. ## runtime.Pinner: keep real Go memory in place Some C APIs do not want a token. They want to write into your buffer between calls, or to walk a struct you allocated. Then you need a genuine Go pointer to survive on the C side, which is what `runtime.Pinner` is for. ```go var pinner runtime.Pinner defer pinner.Unpin() pinner.Pin(obj) ``` `Pin` marks the object so the collector will neither free nor move it, and while it is pinned C may legally hold a pointer to it. Pinning also unlocks the other rule: a struct that contains pointers to pinned objects becomes passable, where an unpinned one is rejected. `Unpin` releases **everything** that `Pinner` has pinned — it is all-or-nothing per `Pinner`, which is why the usual shape is one `Pinner` per scope with a deferred `Unpin`. A `Pinner` must not be copied after first use. The costs are real. A pinned object cannot be reclaimed no matter how dead it is, so a `Pinner` whose `Unpin` is skipped on an error path is a leak with the same shape as a missing `free`. And the lifetime is now yours to reason about: the moment `Unpin` runs, every pointer C still holds becomes a stale pointer, and using one is corruption rather than a panic. ## Choosing Ask one question: **does C dereference this memory, or only carry it?** - Carries it only — a callback context, a session id, a "user data" slot: **Handle**. It is safe by construction, imposes no constraint on the value's shape, and its failure mode (a leaked table entry) is benign compared to a stale pointer. - Dereferences it — an output buffer the codec fills across calls, a struct the C side reads fields from: **Pinner**, or, better where you can afford it, C-allocated memory instead, so the boundary owns its own storage and Go never has to be pinned at all. That third option deserves saying out loud in a review. Pinning is often chosen to avoid a copy that was never measured. If a C codec needs a long-lived working buffer, `C.malloc`-ing it once and reusing it removes both the pin and the copy, at the price of manual freeing that the same `defer` discipline handles. ## Reviewing the first cgo call in a repository What a reviewer should check, in order: is a Go pointer retained past a call at all; if yes, could a `Handle` have replaced it; if a pin is genuinely needed, is the `Unpin` deferred on every path including panics; and is the C side's retention documented, since nothing in the Go code says how long the C library keeps what you gave it. That last one is the item that gets skipped and the one that later costs a week.
- What happens if a per-request cgo.Handle is never deleted?The runtime's handle table keeps the entry, and the entry keeps the Go value reachable, so both leak for the life of the process. It looks like a slow, steady Go heap growth with no obvious owner. The fix is the same discipline as a file handle: `defer h.Delete()` next to the `NewHandle`, and a test that exercises the error paths.
- Why can a Handle carry a closure or a map when a pinned pointer to one would still be awkward?Because nothing about the value crosses the boundary — only an integer does. The value stays in a Go-side table, so its internal pointers are irrelevant to cgo's rules. Pinning, by contrast, keeps one object in place; a value whose fields point at other Go objects would need each of those pinned separately if C were to follow them.
- C still holds a pointer after your Unpin runs. What do you expect to see?Nothing, for a while. The memory may be reused or reclaimed, and the next C read or write through that pointer touches whatever now lives there — corruption whose symptom appears far from the cause, in unrelated data. That is why the Unpin must be scoped to outlive every C-side reference, and why the C library's retention behaviour belongs in a comment.
- When would you use neither, and allocate on the C side instead?When the C library needs a long-lived working buffer that it, not Go, logically owns. Allocating it once with C.malloc and freeing it at teardown removes the pin, removes the per-call copy, and puts the lifetime in one place. You trade automatic memory management for an explicit free that a deferred call handles cleanly.
saying these in an interview costs you the question
- Passing a Go pointer as the callback context and hoping
- Never calling Delete on a handle created per request
- Assuming Unpin releases only the last pinned object
- Pinning to avoid a copy nobody measured
- Copying a Pinner value after it has been used