How to Call C from Go with Cgo
You are building a Go service. You need to compress images using a specific C library because the Go version is too slow or does not exist. The C library is battle-tested, optimized, and rewriting it would take months. You do not rewrite. You bridge the gap. Go provides a mechanism to call C code directly from your Go program. This mechanism is called cgo.
Cgo is the Foreign Function Interface for Go. It allows Go code to call C functions and access C data structures. It works by generating wrapper code at compile time. The bridge is not free. Crossing it involves type conversion, memory management overhead, and strict rules about pointers. Understanding cgo means understanding the boundary between Go's safe, garbage-collected world and C's raw, manual memory model.
The bridge and the preamble
Cgo operates through a pseudo-package named C. To use it, you write C code or include directives in a comment block immediately before import "C". This comment block is the C preamble. The cgo tool parses this comment, extracts the C code, and uses it to generate Go bindings.
Think of cgo as a customs checkpoint. Go values are citizens with passports. C values are foreign nationals. To pass a value across, cgo inspects it, converts the type, and stamps the paperwork. Some values pass easily. Others get flagged. Pointers are the most scrutinized items. The garbage collector manages Go memory. It does not know about C memory. If you pass a Go pointer to C, and C stores that pointer, the GC might move the Go object. C will then point to garbage. The runtime detects this and panics.
Minimal example
Here is the smallest program that calls C. It includes a header and calls a standard library function.
package main
/*
#include <stdio.h>
*/
import "C"
func main() {
// C. prefix accesses the C symbol table generated by cgo.
// String literals are automatically converted to C char arrays.
C.printf("Hello from C\n")
}
The comment block tells cgo to include stdio.h. The import "C" line triggers the cgo preprocessor. Without the import, the comment is just a comment. With the import, cgo processes the file. The C. prefix in the function call tells the Go compiler to look up the symbol in the C namespace.
What happens at build time
When you run go build, the toolchain invokes cgo before the standard compiler. The process follows a specific sequence.
cgoparses the Go file and finds the C preamble.- It extracts the C code and generates a C file containing wrapper functions.
- It generates a Go file with type definitions for C types like
C.int,C.char, andC.size_t. - It compiles the generated C file into an object file using the system C compiler.
- It links the object file with the Go binary.
The result is a single executable that contains both Go and C code. The C code runs in the same process. You can see the generated files by running go build -work. This flag prints the temporary directory where cgo stores its output. Inspecting these files reveals how cgo translates types and generates wrappers.
Realistic example: passing data and handling memory
Calling C with data requires type conversion. Go strings are immutable and length-prefixed. C strings are mutable and null-terminated. You cannot pass a Go string directly to a C function expecting char*. You must allocate C memory and copy the data.
Here is a realistic example that passes a string to C, calls a function, and handles the result. It also demonstrates memory management.
package main
/*
#include <string.h>
#include <stdlib.h>
// C function returns the length of the string.
// Returns -1 if the input is NULL.
int get_length(const char* s) {
if (s == NULL) return -1;
return (int)strlen(s);
}
*/
import "C"
import "unsafe"
func main() {
// Go string to C string.
// CString allocates memory on the C heap and copies the Go string.
cs := C.CString("Measure this")
// Always free C allocated memory.
// defer ensures cleanup even if panics occur later.
defer C.free(unsafe.Pointer(cs))
// Call C function.
// C.int converts Go int to C int type.
length := C.get_length(cs)
// Convert C.int back to Go int.
goLength := int(length)
// Use goLength in Go code.
}
The C.CString function allocates memory on the C heap. It returns a *C.char. You must free this memory using C.free. If you forget, you leak memory. The garbage collector will never reclaim C heap memory. The defer statement ensures the memory is freed when the function returns.
The unsafe.Pointer conversion is necessary because C.free expects a void*. Go's type system requires an explicit conversion to unsafe.Pointer to cross the boundary. This is a convention in cgo code. Every call to C.free takes an unsafe.Pointer.
Building with external libraries
Most cgo usage involves linking against external C libraries. You use #cgo directives in the preamble to pass flags to the C compiler and linker.
package main
/*
#cgo CFLAGS: -I/usr/local/include/mylib
#cgo LDFLAGS: -L/usr/local/lib -lmylib
#include "mylib.h"
*/
import "C"
func main() {
// Call functions from mylib using C. prefix.
C.mylib_init()
}
The #cgo CFLAGS directive passes flags to the C compiler. This is where you add include paths. The #cgo LDFLAGS directive passes flags to the linker. This is where you specify library paths and library names. The #include directive includes the header file. The cgo tool reads these directives and applies them during the build.
Convention aside: cgo directives are sensitive to whitespace. The #cgo line must start with #cgo and have a space after it. The value follows immediately. If the directive is malformed, the compiler rejects the program with cgo: invalid directive.
Pitfalls and runtime errors
Cgo introduces pitfalls that do not exist in pure Go code. The most common issues involve pointers, memory leaks, and build environment configuration.
Pointer passing rules
The Go runtime enforces strict rules about passing pointers to C. You cannot pass a Go pointer to C if C might store it beyond the duration of the call. You cannot pass a Go pointer to C if the pointer points to another Go pointer. The runtime checks these rules and panics if they are violated.
If you pass a slice of pointers to C, the compiler rejects the program with cgo argument has Go pointer to Go pointer. If C stores a Go pointer and you return from the call, the runtime panics with cgo used Go pointer after return. To fix this, copy the data to C memory before passing it. Use C.CString for strings. Use C.malloc and memcpy for structs.
Memory leaks
C memory does not get garbage collected. Every C.malloc, C.CString, or library function that allocates memory must have a corresponding C.free. If a goroutine calls cgo and leaks memory, the leak persists until the process exits. The worst goroutine bug is the one that never logs. A silent memory leak in cgo code can grow slowly and crash the service days later.
Build environment
Cgo requires a C toolchain. If you set CGO_ENABLED=0, cgo is disabled. This is common for cross-compilation or minimal Docker images. If your code uses cgo and you disable it, the build fails with cgo: C type ... not supported or import "C" not allowed when CGO_ENABLED=0. Always check your build environment. Cross-compiling Go binaries that use cgo requires installing the appropriate C cross-compiler and setting CC and CXX environment variables.
Error handling
C functions often return error codes or set errno. Go functions return errors. You should wrap C error codes into Go errors. This makes the error handling consistent with the rest of your codebase.
// CallCFunc calls a C function and returns a Go error.
func CallCFunc() error {
// C function returns 0 on success, -1 on error.
ret := C.some_c_function()
if ret == -1 {
return fmt.Errorf("c function failed")
}
return nil
}
This pattern translates C conventions into Go conventions. It keeps the error handling visible and explicit.
Decision matrix
Use cgo when you must reuse an existing C library that has no Go equivalent. Use cgo when you need to interface with system APIs that lack Go wrappers. Use cgo when performance profiling shows a specific C routine is faster than any Go implementation and the overhead is acceptable. Use a pure Go rewrite when you want cross-compilation without a C toolchain. Use a pure Go rewrite when you need to deploy to environments where cgo is disabled. Use a separate process with RPC when the C code is unstable or you want to isolate crashes. Use WebAssembly when you need to run C code in a browser or sandboxed environment.
Cgo is a bridge, not a tunnel. Cross it only when necessary. Memory you allocate in C stays in C. Free it or leak it. The garbage collector does not see C memory. Respect the boundary.