CGO: Crossing the Go-C Boundary
Go compiles to a self-contained binary by default, but cgo lets a package call C code and link against native libraries.
Search across all documentation pages
Go compiles to a self-contained binary by default, but cgo lets a package call C code and link against native libraries.
That bridge is powerful and expensive: every crossing changes how the runtime schedules work, how you build, and how you reason about memory.
import "C" package compiles Go together with C via a generated shim, so Go functions can call C and C can call exported Go..so files.syscall/x/sys ports, RPC sidecars, WebAssembly paths, and FFI safety rules.A normal Go program is compiled entirely by gc (the Go compiler) and linked into one binary.
cgo inserts a second compiler front: cmd/cgo reads Go files that contain import "C" and emits C glue code.
The Go compiler then compiles your package alongside that glue, and the platform C linker pulls in libc and any libraries you name in #cgo LDFLAGS.
The mental model is a three-layer sandwich:
Go code <--> cgo-generated shim <--> C library / libc
The C pseudo-package in Go source is not a real Go package.
Types like C.int and functions like C.sqrt are declarations cgo maps to C symbols.
Comments immediately above import "C" are cgo directives: #include, #cgo CFLAGS, #cgo LDFLAGS, and #define lines that configure the C side.
Without import "C", a file is pure Go even if it lives next to cgo files.
Build tags such as //go:build cgo (and the legacy // +build cgo) let you ship pure-Go fallbacks when CGO_ENABLED=0, which is common in CI, static containers, and cross-compiles.
Each cgo call from Go into C runs on a dedicated OS thread.
While inside C, that thread is not running Go code, so the scheduler may spawn another thread so other goroutines keep progressing.
Heavy cgo traffic therefore increases OS thread count and context-switch cost.
This is why a tight loop calling C can cap throughput below what pure Go achieves on the same hardware.
Crossing the boundary also triggers pointer checks.
Go's garbage collector must not move memory that C still references.
Rules documented in cmd/cgo restrict passing Go pointers into C in ways that could outlive the call or hide pointers inside non-pointer Go values.
Violations panic at runtime in checked builds.
Building with cgo enabled requires a working C toolchain (gcc or clang on Linux, Xcode CLI tools on macOS, MinGW on Windows).
CGO_ENABLED=0 disables cgo entirely; go build then fails on packages that import C unless alternate files exist.
Static linking, musl vs glibc, and cross-compiling to GOOS=linux GOARCH=arm64 from macOS all become release-engineering tasks instead of a single go build line.
/*
#include <stdio.h>
*/
import "C"
func greet(name string) {
cs := C.CString(name)
defer C.free(unsafe.Pointer(cs))
C.printf(C.CString("hello %s\n"), cs)
}The snippet shows the recurring pattern: convert Go strings to C (C.CString), free C allocations (C.free), and keep C calls short so pointer lifetimes stay obvious.
Teams usually isolate cgo behind a small internal package with a pure-Go API.
Callers depend on Go types and errors; only the wrapper imports C.
That boundary makes it possible to test logic without C in most packages and to swap implementations later.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| cgo in-process | Lowest call latency, shared address space | Thread + build complexity | Hot path must stay in-process |
| RPC to C/C++ sidecar | Independent release and crash isolation | Network/IPC overhead | Legacy code you cannot link safely |
Pure-Go port or x/sys | Simple builds, easy cross-compile | Upfront rewrite or incomplete coverage | Syscalls and algorithms with Go equivalents |
| Rewrite in Go | Idiomatic APIs, one toolchain | Time and validation cost | Libraries with manageable scope |
Security and supply-chain reviews treat cgo code as native code: buffer overflows in C become your process's problem.
Fuzzing and sanitizers (ASan/UBSan) on the C side belong in the same quality bar as Go tests.
Observability splits across runtimes: Go profiles show cgo time under runtime.cgocall, but C hotspots need pprof on the C library or external profilers.
For containers, document whether the image needs libc, libstdc++, or vendor .so files and whether you ship CGO_ENABLED=0 variants for scratch images.
C.CString allocates in the C heap; you must C.free it (or hand ownership to C with a documented contract).CGO_ENABLED=0 builds fail unless you choose alternate drivers.Any Go file with import "C" and the cgo comment block above it.
The go tool invokes cmd/cgo automatically during go build and go test.
Yes - only files that import C participate in cgo.
Keep import "C" files few and push logic into pure Go siblings.
The Go scheduler cannot preempt arbitrary C code safely.
cgo runs C on a thread the runtime manages so goroutines and signals still behave predictably.
An environment variable defaulting to 1 when a C toolchain is detected.
Set CGO_ENABLED=0 to force pure Go builds for static binaries and simpler CI.
No - many syscalls are wrapped in pure Go (golang.org/x/sys) or the standard library.
Reach for cgo when no safe Go wrapper exists or a vendor ships only a C SDK.
//go:build cgo files compile only when cgo is on; !cgo files provide stubs.
This pattern powers portable modules like sqlite drivers and terminal libraries.
Yes, with //export functions and careful use of runtime.LockOSThread when C holds thread-local state.
Callbacks are harder than one-way calls - see the exporting guide in this section.
Yes - #cgo LDFLAGS: -L${SRCDIR}/lib paths are common for vendored .a files.
You still own licensing and platform-specific artifacts in vendor/ or internal/.
You need arm64 builds of every .so and a matching cross compiler if you cross-build.
Pure Go artifacts do not have that coupling.
When a maintained pure-Go library exists, when RPC isolation is acceptable, or when you need tiny static binaries without libc.
Measure latency before committing to in-process cgo.
CPU profiles label runtime.cgocall.
Also watch thread count and tail latency under load; both rise when C sections run long.
For browser and some edge hosts, yes - Go can target wasm with GOOS=js or wasip1.
Native OS integration still needs cgo or pure-Go syscall wrappers.
#cgo flagsStack 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