What are cgo's rules for passing a Go pointer into C code?
answer
- Two questions: what is inside, for how long
- Pointers hide inside slices and interfaces
- The call is the lease term
- A field is itself; an element is everything
- Pinning is the only opt-out
basics
~20 sPass a Go pointer to C only if the memory it points at holds no Go pointers, and C must not keep it after the call returns. C may never store a Go pointer into Go memory.
solid answer
~50 sTwo rules carry most of it. First, a Go pointer may be passed only if the Go memory it points at holds no Go pointers of its own — so `&buf[0]` on a `[]byte` is fine, while a pointer to a struct with a slice, string, map or interface field is not. Second, C may use that pointer only for the duration of the call; it may not stash it in a C struct or a global and dereference it later. Two more finish the set: C may never write a Go pointer into Go memory, and a Go function called from C may not return a Go pointer. Granularity matters — a pointer to a struct *field* only implicates that field, but a pointer to a slice element implicates the whole backing array. Since Go 1.21 `runtime.Pinner` lifts the first two rules for memory you explicitly pin.
code
go · 11 linestype chunk struct {
Seq int
Data []byte // a Go pointer lives inside this field
}
var c chunk
// Rejected: &c points at memory that contains a Go pointer (c.Data).
// Allowed: &c.Seq points at one field, and that field holds no pointer.
// Allowed: &c.Data[0] points into a byte array, which holds no pointers,
// but the whole backing array is what the rule is judged on.go deeper
Be ready to state the two headline rules: no Go pointers inside the memory you pass, and C must forget the pointer when the call returns. Knowing that a []byte is passable and a struct with a slice field is not already puts you ahead.
Explain why slices, strings, maps and interfaces count as containing pointers, and get the granularity right: a struct field implicates only the field, a slice element implicates the whole backing array. Mention that enforcement is a runtime check, not a compile-time one.
Show that you know what a violation actually costs — silent corruption discovered far from the cause, not a clean crash — and that you would review a C API's retention behaviour before approving the call. Be able to lay out copy, one-call pointer, and pin or token as the three designs.
Be ready to set the boundary policy: which of the three designs is the default in this codebase, what has to be true for an exception, and how much C surface area you are willing to own at all given that these rules cannot be enforced by the compiler.
## The problem the rules solve Go memory is managed: the collector reclaims what it cannot reach, and a goroutine's stack is moved when it grows. C memory is not managed and C code is invisible to both mechanisms. If a C library holds a pointer into Go memory, nothing in the runtime knows about that reference, so the memory can be reclaimed or relocated while C still believes the pointer is good. The result is not a clean panic — it is a write through a stale address, corrupting whatever now occupies it, discovered minutes later somewhere unrelated. cgo's pointer-passing rules exist to make that impossible by construction. ## The four rules **1. What you may pass.** Go code may pass a Go pointer to C provided the Go memory it points to contains no Go pointers to unpinned memory. "Contains a Go pointer" is broader than it first sounds: pointer fields obviously count, but so do slices, strings, maps, channels, function values and interface values, because each of those carries a pointer inside its representation. A `*int`, a `*[64]byte` or `&buf[0]` for a `[]byte` are all fine. A `**Item`, a `*[]string` or a pointer to a struct with a `Data []byte` field are all rejected. **2. How long C may hold it.** C may not keep a copy of a Go pointer after the call returns. Reading and writing through it during the call is fine; storing it in a C-side context struct, a global, or a linked list for use on the next call is not. This is the rule people break first, because C APIs are full of `set_context`-shaped functions. **3. What C may write.** C code may not store a Go pointer into Go memory, even temporarily. If you hand C a `*Node` and the C code writes another Go pointer into one of its fields, the write bypasses the write barrier the collector depends on. **4. What Go may return.** A Go function exported to C with `//export` may not return a Go pointer, for the same reason as rule 2 — C would hold a reference the runtime cannot see. ## Granularity: the detail that decides real cases The rules are stated in terms of "the Go memory in question", and what that means depends on what you took the address of: - **A struct field.** The memory in question is the memory occupied by that field, not the whole struct. So `&job.Length` is passable even if `job` also has a `Data []byte` field elsewhere in it. - **A slice or array element.** The memory in question is the **entire** array, or the entire backing array of the slice — not just that element. So `&items[0]` on a `[]*Item` is rejected outright, because the backing array is full of Go pointers, even though you only pointed at one element. That asymmetry is the single most commonly missed part of the rules, and it is worth stating explicitly in an interview. ## The pinning exception Go 1.21 added `runtime.Pinner`, and the rules were reworded around it: the restriction is on Go pointers to **unpinned** memory. Pin an object and C may legally hold a pointer to it across calls, and a struct containing pointers to pinned objects becomes passable. Pinning is scoped — `defer pinner.Unpin()` — and every pinned object stays alive and in place until then. ## What is *not* restricted C pointers are unrestricted in the other direction: Go may hold a `*C.char` or an opaque `*C.codec_ctx` indefinitely, store it in a struct, and pass it around, because C memory is neither collected nor moved by Go. The asymmetry catches people who assume the boundary is symmetric. ## Enforcement The rules are checked dynamically, not by the compiler. At each cgo call the runtime inspects pointer arguments and aborts with a message naming a Go pointer to an unpinned Go pointer when it finds a violation. Those checks are cheap and therefore incomplete — they cannot see what C does with a pointer after the call returns, which is precisely rule 2. That is why the answer to "how do I know we obey the rules" is a code-review answer as much as a tooling one. ## The shape of a correct design In practice you get three options and should name them in this order: **copy** across the boundary and own nothing shared; pass a pointer to **pointer-free** memory (a `[]byte`, a numeric struct) for the duration of one call only; or, when C genuinely must retain a reference, hand it an integer token instead of a pointer, or pin the memory explicitly. Reaching for the third option without noticing you have entered it is how a repository's first cgo call becomes its first memory-corruption bug.
- Why is passing &items[0] rejected when items is a []*Item, even though you only pointed at one element?Because for a slice or array element the memory in question is the entire backing array, not the element. The array is full of Go pointers, so the pointer you pass leads to Go pointers and the rule rejects it. The field rule is the opposite: a pointer to a struct field implicates only that field.
- May Go hold a C pointer across many calls, or does a mirror-image rule apply?Go may hold it indefinitely. The restrictions are one-directional because they exist to protect Go's collector and its movable stacks; C memory is neither collected nor moved by Go, so a `*C.char` or an opaque C context handle can be stored in a Go struct and used for the life of the process. You are then responsible for its manual lifetime instead.
- Are these rules enforced by the compiler?No. They are dynamic checks the runtime performs on pointer arguments at each cgo call, and they abort the program when they fire. Being dynamic, they only see what actually runs, and they cannot observe what C does with a pointer after the call returns — so rule two, the retention rule, is largely on you and your reviewers.
- A C API wants a context pointer it will pass back to your callback. What do you hand it?Not a Go pointer. Either an integer token from `runtime/cgo.Handle`, which you convert back to the Go value inside the callback, or C-allocated memory you fill in yourself. If C truly must dereference Go memory across calls, pin it with `runtime.Pinner` and scope the `Unpin` to the lifetime of the C-side reference.
saying these in an interview costs you the question
- Thinking any Go pointer is fine as long as C does not free it
- Missing that a slice, string or interface field contains a pointer
- Assuming the rule is symmetric for C pointers held by Go
- Letting C store the pointer in a context struct for later
- Believing the compiler rejects violations at build time