CGO enables Go programs to call C code and vice versa. It's a powerful but complex feature that should be used judiciously.
What is CGO?
CGO is a Go tool that allows Go programs to interoperate with C code. It enables:
CGO exists to integrate legacy code (existing C libraries), use optimized C code for performance-critical paths, access low-level system functionality, and use C libraries not available in Go.
When to use CGO
When NOT to use CGO
CGO uses a special import "C" that triggers C code compilation:
package main
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
func main() {
C.hello()
}
The import "C" is special—it's not a real package. Comments immediately before it are treated as C code and compiled.
Basic C function call
package main
/*
#include <stdio.h>
#include <stdlib.h>
int add(int a, int b) {
return a + b;
}
*/
import "C"
import "fmt"
func main() {
result := C.add(10, 20)
fmt.Printf("Result: %d\n", result) // Result: 30
}
C types are automatically converted: int → C.int, char* → *C.char, void* → unsafe.Pointer, float → C.float.
Working with C strings
package main
/*
#include <string.h>
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Go string to C string
goStr := "Hello, World!"
cStr := C.CString(goStr)
defer C.free(unsafe.Pointer(cStr)) // Must free C string
// Use C string
length := C.strlen(cStr)
fmt.Printf("Length: %d\n", int(length))
}
C.CString() allocates C memory for a string (must be freed), and C.free() frees C-allocated memory. C strings are manually managed, so you must free them to avoid memory leaks.
Using C libraries
package main
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
import "fmt"
func main() {
x := 2.0
result := C.sqrt(C.double(x))
fmt.Printf("sqrt(%f) = %f\n", x, float64(result))
}
#cgo directives specify build flags: LDFLAGS for linker flags (e.g., -lm for math library), CFLAGS for compiler flags, and pkg-config for library detection.
Working with C structs
package main
/*
typedef struct {
int x;
int y;
} Point;
Point make_point(int x, int y) {
Point p;
p.x = x;
p.y = y;
return p;
}
*/
import "C"
import "fmt"
func main() {
p := C.make_point(10, 20)
fmt.Printf("Point: (%d, %d)\n", int(p.x), int(p.y))
}
C structs are accessed through the C package. Field access uses dot notation, but types are C types.
Calling C functions with callbacks
package main
/*
typedef void (*Callback)(int);
void call_callback(Callback cb, int value) {
cb(value);
}
*/
import "C"
import "fmt"
//export goCallback
func goCallback(value C.int) {
fmt.Printf("Callback called with: %d\n", int(value))
}
func main() {
C.call_callback((C.Callback)(C.goCallback), 42)
}
Callbacks are complex because they require the //export directive and careful type conversions. They're one of the more complex CGO patterns.
You can also call Go functions from C code.
Basic Go function callable from C
package main
/*
#include <stdio.h>
extern void GoFunction(int value);
void call_go() {
GoFunction(42);
}
*/
import "C"
//export GoFunction
func GoFunction(value C.int) {
fmt.Printf("Called from C with: %d\n", int(value))
}
func main() {
C.call_go()
}
The //export directive makes a Go function callable from C. The function name in C must match the Go function name.
Naming conventions
GoFunction becomes C function GoFunctionexternPassing complex data
package main
/*
typedef struct {
int x;
int y;
} Point;
extern void ProcessPoint(Point p);
*/
import "C"
import "fmt"
//export ProcessPoint
func ProcessPoint(p C.Point) {
fmt.Printf("Point: (%d, %d)\n", int(p.x), int(p.y))
}
Best practices
1. Minimize CGO usage
// ✅ GOOD: Use Go when possible
func add(a, b int) int {
return a + b
}
// ❌ AVOID: Unnecessary CGO
/*
int add(int a, int b) {
return a + b;
}
*/
import "C"
// C.add(a, b) // Overhead not worth it
2. Isolate CGO code
// Separate package for CGO
package cwrapper
/*
#include "library.h"
*/
import "C"
func CallLibrary() {
C.library_function()
}
// Main package uses wrapper
package main
import "myproject/cwrapper"
func main() {
cwrapper.CallLibrary()
}
Isolating CGO code helps because CGO code requires a C compiler (build complexity), isolated CGO is easier to handle for cross-compilation, and you can mock the CGO wrapper in tests.
3. Always free C memory
// ✅ GOOD: Always free
cStr := C.CString("hello")
defer C.free(unsafe.Pointer(cStr))
// ❌ BAD: Memory leak
cStr := C.CString("hello")
// Forgot to free!
4. Use unsafe.Pointer carefully
// ✅ GOOD: Type conversion
cPtr := C.malloc(100)
defer C.free(cPtr)
goSlice := (*[100]byte)(unsafe.Pointer(cPtr))[:]
// ⚠️ DANGEROUS: Must ensure memory is valid
5. Handle errors properly
// C functions often return error codes
result := C.some_function(&errCode)
if errCode != 0 {
return fmt.Errorf("C function failed: %d", errCode)
}
Limitations
1. Performance overhead
CGO calls have overhead:
When overhead matters
2. Cross-compilation complexity
CGO requires:
3. Build dependencies
CGO adds dependencies:
4. Garbage collection
C code can't access Go's garbage collector:
5. Thread safety
C code runs on OS threads:
6. Debugging complexity
CGO code is harder to debug:
Alternatives to CGO
1. Pure Go libraries
// ✅ PREFER: Pure Go when available
import "github.com/go-library/package"
// ❌ AVOID: CGO wrapper if Go alternative exists
import "C"
2. System calls
// ✅ GOOD: Use syscall package
import "syscall"
syscall.Syscall(...)
// ⚠️ CONSIDER: CGO only if syscall insufficient
3. FFI libraries
Libraries like github.com/ebitengine/purego provide FFI without CGO.
When CGO is necessary
Example: When CGO makes sense
// Using a well-established C library (e.g., SQLite)
package main
/*
#cgo LDFLAGS: -lsqlite3
#include <sqlite3.h>
*/
import "C"
// ... SQLite wrapper code
CGO is a powerful tool for interoperability, but it comes with complexity and overhead. Use it when necessary, but prefer pure Go solutions when available. Isolate CGO code, manage memory carefully, and be aware of the limitations.