CGO Basics
10 examples to get you started with CGO & Interop - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later and a C toolchain (
gccorclang; Xcode CLI tools on macOS). - Verify cgo:
go env CGO_ENABLEDshould print1when a compiler is present. - Create a module:
mkdir cgodemo && cd cgodemo && go mod init example.com/cgodemo. - Build with cgo on by default; force pure Go with
CGO_ENABLED=0 go buildwhen testing fallbacks.
Basic Examples
1. Smallest cgo program
import "C" with a C comment block enables cgo for this file.
package main
/*
#include <stdio.h>
*/
import "C"
func main() {
C.puts(C.CString("hello from cgo"))
}- The
/* ... */block immediately aboveimport "C"is passed to the C compiler. C.putsmaps to the C standard library function.C.CStringallocates C memory; production code shoulddefer C.free(covered in example 5).
Related: CGO: Crossing the Go-C Boundary - What cgo costs at runtime
2. Call a C math function
Linking uses libc symbols included via headers.
package main
/*
#include <math.h>
*/
import "C"
import "fmt"
func main() {
x := C.sqrt(2)
fmt.Println(float64(x))
}- Numeric literals convert to C types when passed to
C.sqrt. - Cast results back with
float64(x)for Go APIs. - No extra
LDFLAGSneeded forlibmon most platforms when using standard headers.
Related: Calling C from Go & Exporting Go to C - Type conversions and directives
3. #cgo CFLAGS and #cgo LDFLAGS
Directives in the comment block configure compile and link flags.
package main
/*
#cgo CFLAGS: -Wall
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
func main() {
_ = C.cos(1)
}#cgo CFLAGSadds C compiler flags for this package only.#cgo LDFLAGSpasses flags to the linker (-lmpulls libm where required).- Platform-specific lines use
#cgo linux LDFLAGS:or#cgo darwin CFLAGS:.
Related: Calling C from Go & Exporting Go to C - Full directive reference
4. Include a local C header
${SRCDIR} expands to the directory containing the Go source file.
package main
/*
#cgo CFLAGS: -I${SRCDIR}/include
#include "greet.h"
*/
import "C"
func main() {
C.greet_from_c()
}- Place
include/greet.handgreet.c(or a prebuilt.a) beside the Go file. - Add
#cgo LDFLAGS: ${SRCDIR}/greet.owhen compiling a local.cfile into the package. - Keep headers minimal; large C trees belong in a dedicated internal package.
Related: CGO & Interoperability Best Practices - Isolate cgo behind small packages
5. C.CString and C.free
C allocations are not garbage-collected by Go.
package main
/*
#include <stdlib.h>
#include <stdio.h>
*/
import "C"
import "unsafe"
func printC(s string) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
C.puts(cs)
}
func main() {
printC("freed after use")
}C.CStringcopies bytes and adds a NUL terminator in the C heap.- Always pair with
C.freeunless C takes ownership per API contract. - Import
unsafeonly for theunsafe.Pointercast required byC.free.
Related: FFI Safety: Pointers, C.CString & Free - Ownership rules in depth
6. Convert Go []byte to C
Use C.CBytes for binary buffers and free when done.
package main
/*
#include <stdlib.h>
#include <string.h>
*/
import "C"
import "unsafe"
func main() {
data := []byte{0x01, 0x02, 0x03}
ptr := C.CBytes(data)
defer C.free(ptr)
_ = C.memcpy(ptr, ptr, C.size_t(len(data)))
}C.CBytesallocateslen(data)bytes; contents are copied.C.GoBytescopies C memory back into a Go slice.- For hot paths, prefer pinned buffers and documented lifetime rules over repeated alloc/free.
Related: FFI Safety: Pointers, C.CString & Free - Slice and pointer passing
7. Build tags: cgo vs pure Go
Ship a stub when CGO_ENABLED=0.
//go:build cgo
package demo
/*
#include <stdint.h>
*/
import "C"
func AddOne(n int) int {
return int(C.int(n) + 1)
}//go:build !cgo
package demo
func AddOne(n int) int {
return n + 1
}//go:build cgofiles compile only when cgo is enabled.!cgofiles let CI and cross-compiles succeed without a C compiler.- Keep exported Go signatures identical across files so callers stay unaware.
Related: syscall & Pure-Go Alternatives to CGO - Fallback strategies
Intermediate Examples
8. Environment variables CGO_CFLAGS and CGO_LDFLAGS
Override flags globally for a single build without editing source.
CGO_CFLAGS="-O3 -I/opt/vendor/include" \
CGO_LDFLAGS="-L/opt/vendor/lib -lvendor" \
go build -o app .CGO_CFLAGSandCGO_LDFLAGSappend to per-package#cgolines.- Useful in CI matrices that point at cached SDK paths.
- Document required values in README or build scripts; do not rely on undocumented machine paths.
Related: CGO & Interoperability Best Practices - Reproducible build habits
9. Check whether cgo is enabled at compile time
Use build constraints in tests and small probe packages.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("cgo enabled:", cgoEnabled())
fmt.Println("GOOS/GOARCH:", runtime.GOOS, runtime.GOARCH)
}//go:build cgo
package main
func cgoEnabled() bool { return true }//go:build !cgo
package main
func cgoEnabled() bool { return false }- Split
cgoEnabledacross tagged files instead of parsinggo envat runtime. - Release pipelines should build both variants when consumers expect pure Go binaries.
- Log the active variant in support bundles to debug "works on my laptop" reports.
Related: Interop Decision Guide: CGO vs RPC vs Rewrite - When cgo is worth the build cost
10. Wrap a C function with a Go-friendly API
Hide import "C" behind an internal package.
// internal/native/add.go
package native
/*
#include "add.h"
*/
import "C"
func Sum(a, b int) int {
return int(C.add(C.int(a), C.int(b)))
}// main.go
package main
import (
"fmt"
"example.com/cgodemo/internal/native"
)
func main() {
fmt.Println(native.Sum(40, 2))
}- Only
internal/nativeimportsC;mainstays pure Go. - Tests can target
nativewith build tags while fuzzing logic in pure Go wrappers. - Errors from C should convert to Go
errorvalues at this boundary.
Related: Calling C from Go & Exporting Go to C - Exporting Go back to C
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).