CGO & Interoperability Best Practices
Isolate cgo behind small packages and test on all targets.
Search across all documentation pages
Isolate cgo behind small packages and test on all targets.
These rules keep native interop shippable: clear ownership, reproducible builds, and escape hatches when CGO_ENABLED=0 is required.
import "C" to application packages.unsafe, #cgo, or vendor .so files.CGO_ENABLED=1 and CGO_ENABLED=0 when the module claims portable builds.staticcheck, govulncheck) and documented exceptions in ADRs.import "C" to one internal package per native dependency. Application code depends on Go types and errors only.C.* types, at module boundaries. Callers should not need cgo knowledge to compile.//go:build !cgo fallbacks or clear errors when cgo is mandatory. CI and cross-compiles must fail loudly, not silently misbehave..so names, licenses, and supported OS/arch matrix.init functions that call C. Lazy-init on first use simplifies tests and startup ordering.C.CString/C.CBytes with C.free on all paths. Use defer immediately after allocation succeeds.C.GoString/C.GoBytes unless C retains ownership by contract.//export callbacks with mutexes or channels. Assume C calls from arbitrary OS threads.runtime.LockOSThread only around thread-local C setup, not entire handlers. Unlock promptly to preserve scheduler throughput.runtime.cgocall before micro-optimizing Go code.//export functions. Uncaught panics across the FFI boundary abort the process.CGO_CFLAGS, CGO_LDFLAGS, and pkg-config packages for CI images. Reproducible builds beat laptop-only paths.go test ./... with both cgo on and off when applicable. Catch missing build tags early.golang.org/x/sys and pure ports before new cgo. Simpler consumer experience and smaller attack surface.import "C" files with !wasm.Often one directory per vendor SDK with Go types mirroring the 10-20 calls you actually use.
Delete unused C headers from the comment block to speed builds.
No - keep C.* internal.
Public APIs use Go slices, strings, and structs.
C toolchain metapackage, pkg-config, vendor dev headers, matrix for CGO_ENABLED values, and native tests for pointer ownership.
Check thread safety, panic recovery, allocation frees, and whether C holds pointers after return.
Require tests that invoke callbacks from multiple threads when C allows it.
When the module explicitly documents cgo as required and all consumers agree.
Open-source libraries should try harder to offer pure-Go paths.
Practices assume you already chose in-process cgo.
Use the decision guide first when strategy is still open.
Yes when upstream permits - pin hashes and document platform slices in ${SRCDIR} paths.
Verify licenses permit redistribution.
Rising OS thread count, runtime.cgocall in CPU profiles, RSS growth from C heap leaks, and tail latency under load.
Point them to Basics and FFI safety pages; restrict first tasks to pure Go layers above the wrapper.
C changes go through reviewers who read C.
Enable staticcheck and govet; C itself needs separate clang-tidy/ASan.
Pin golangci-lint version per manifest at build.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026