Before Go 1.6, developers could freely pass pointers between Go and C, leading to intermittent crashes when the garbage collector relocated heap objects while C code retained references. To prevent these memory safety violations, Go 1.6 introduced strict pointer passing rules prohibiting C from storing Go pointers after a call returns. The runtime implements a verification system called cgocheck to enforce these constraints during program execution.
C code operates outside the Go runtime's memory management, meaning C allocated memory is invisible to the precise garbage collector. If C stores a pointer to a Go object in a global variable or heap allocation, and that object is later moved by the GC (in future moving GC implementations) or becomes unreachable from Go, dereferencing that pointer causes use-after-free errors or data corruption. Detecting this requires scanning C memory during garbage collection, which is computationally expensive and not feasible in production environments by default.
The runtime provides the GODEBUG=cgocheck environment variable with three modes. Mode 1 (default) checks that arguments passed to C functions do not contain Go pointers to other Go pointers. Mode 2 enables expensive conservative scanning of C stack and heap memory during GC to detect any Go pointers retained in C space, panicking if found. Mode 0 disables all checks. Mode 2 is disabled by default because it imposes significant performance overhead (up to 50% slowdown) by treating C memory as potential pointer roots during every GC cycle.
While building a high-throughput message queue adapter wrapping a C library (librdkafka), we needed to pass message payloads as byte slices from Go to C for asynchronous batch transmission. The C library queued these pointers in an internal linked list for later network transmission by background threads, violating the CGO rule that C cannot retain Go pointers after the initial call returns. During load testing, this caused sporadic segmentation faults when the Go GC reclaimed the underlying array data while C still held references.
Solution 1 - Copy to C heap: We considered copying each message payload to C allocated memory using C.malloc before enqueueing, then freeing it in the delivery callback. Pros: Completely safe, no Go pointer retention, works with any Go version. Cons: Double memory allocation (Go to C), CPU overhead of memcpy for large messages (1MB+), and risk of memory leaks if the C callback fails to free the buffer during network timeouts.
Solution 2 - Use cgo.Handle: We evaluated storing the Go byte slice in a cgo.Handle (an integer token) and passing only the integer to C, requiring a callback to retrieve the data. Pros: Zero-copy for the payload, type-safe reference management, and idiomatic Go 1.17+ pattern for long-term C storage. Cons: Requires implementing a callback mechanism in the C code, increases latency due to extra CGO boundary crossing for data retrieval, and the handle table grows unbounded if C never signals completion.
Solution 3 - Runtime pinning (Go 1.21+): We explored using runtime.Pinner to prevent the GC from moving or collecting the byte slice while C held the reference. Pros: True zero-copy with no C heap allocation, direct memory sharing, and minimal API overhead. Cons: Requires Go 1.21+, manual lifecycle management (risk of memory leaks if Unpin is not called in all error paths), and debugging pinned memory is difficult as it appears as lingering heap objects in profiles.
We selected cgo.Handle (Solution 2) because the adapter architecture already required a delivery confirmation callback. This approach eliminated data copying for our 100MB/s throughput requirement while maintaining safety across Go versions. We added explicit handle deletion in both success and error callbacks to prevent leaks.
The system achieved stable 99.9th percentile latencies under 10ms and processed over 500k messages/second in production. It passed week-long stress tests with GODEBUG=cgocheck=2 enabled to verify no pointer violations. Memory profiles confirmed zero leaks from handle accumulation due to proper cleanup in all code paths.
Why does the default cgocheck=1 mode fail to detect Go pointers stored in C global variables after the call returns?
The default mode only validates the immediate arguments and return values crossing the CGO boundary for pointer-to-pointer violations; it does not scan C memory (global variables, heap, or stack) for retained Go pointers. Only GODEBUG=cgocheck=2 enables conservative scanning of C memory during garbage collection to detect such retentions. This expensive check is disabled by default because it requires treating all C memory as potential GC roots, significantly increasing pause times and CPU usage during collection cycles.
How does cgo.Handle prevent the garbage collector from reclaiming the referenced Go value while the C code holds the integer token?
cgo.Handle stores the Go value in a internal runtime map (in the runtime/cgo package) using the integer as a key. Since the map maintains a reference to the value, the garbage collector marks it as reachable during root scanning and will not reclaim the memory. The integer token passed to C contains no pointer metadata, so C can store it indefinitely without interfering with Go's memory management. When C invokes the callback or Go explicitly deletes the handle, the map entry is removed, dropping the reference and allowing normal collection.
What specific panic indicates a CGO pointer passing violation during a function call, and what runtime flag modifies its detection sensitivity?
The runtime emits runtime error: cgo argument has Go pointer to Go pointer when cgocheck=1 detects a pointer to Go memory inside an argument passed to C. For broader detection including pointers stored in C memory, GODEBUG=cgocheck=2 must be enabled, which may produce runtime: cgo result contains Go pointer or similar fatal errors during GC scanning. These panics indicate that C code has violated the contract by retaining or receiving pointers to Go managed memory that could become invalid during garbage collection.