9.4 CGO

CGO enables Go programs to call C code and vice versa. It's a powerful but complex feature that should be used judiciously.

Introduction to CGO

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.

Calling C code from Go

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.

Calling Go from C

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

Passing 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 and Limitations

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.