Web Analytics

CGO Basics

Advanced ~45 min read

CGO enables Go programs to call C code and vice versa. This powerful feature allows you to leverage existing C libraries, optimize performance-critical sections, and integrate with legacy systems. Learn the fundamentals of CGO, from basic function calls to memory management and build configuration.

What is CGO?

CGO is a foreign function interface (FFI) that allows Go code to call C functions and use C types. It's built into the Go toolchain and requires a C compiler.

CGO Use Cases:
  • Legacy Integration - Use existing C libraries
  • Performance - Optimize critical paths with C
  • System APIs - Access OS-specific C APIs
  • Hardware - Interface with device drivers
CGO Tradeoffs:
  • โš ๏ธ Slower build times
  • โš ๏ธ Cross-compilation complexity
  • โš ๏ธ Debugging is harder
  • โš ๏ธ Memory management complexity
  • โš ๏ธ Not pure Go (loses some benefits)

Hello CGO

The basic CGO syntax uses a special comment block before import "C":

Output
Click Run to execute your code
CGO Syntax Rules:
  • C code goes in comment block before import "C"
  • No blank line between comment and import
  • Prefix C functions with C.
  • Use #include for C headers

Type Conversions

Converting between Go and C types requires explicit conversions:

Output
Click Run to execute your code
Go Type C Type Conversion
int int C.int(x)
int32 int32_t C.int32_t(x)
int64 int64_t C.int64_t(x)
float32 float C.float(x)
float64 double C.double(x)
string char* C.CString(s)
[]byte void* C.CBytes(b)

String Handling

Strings require special care because Go strings are immutable and C strings are null-terminated:

Output
Click Run to execute your code
String Conversion Functions:
  • C.CString(s) - Go string โ†’ C char* (allocates, must free)
  • C.GoString(cs) - C char* โ†’ Go string (copies)
  • C.GoStringN(cs, n) - C char* โ†’ Go string (with length)
  • C.CBytes(b) - Go []byte โ†’ C void* (allocates, must free)
  • C.GoBytes(p, n) - C void* โ†’ Go []byte (copies)

Callbacks

Go functions can be called from C using the //export directive:

Output
Click Run to execute your code
Callback Restrictions:
  • Exported functions must use C types
  • Cannot call Go functions that allocate
  • Cannot use goroutines
  • Cannot panic
  • Keep callbacks simple and fast

Memory Management

Understanding memory ownership is critical when using CGO:

Output
Click Run to execute your code
Memory Rules:
  • โœ“ C-allocated memory must be freed with C.free()
  • โœ“ C.CString() allocates - always free
  • โœ“ C.CBytes() allocates - always free
  • โœ“ C.GoString() copies - no free needed
  • โœ“ Don't pass Go pointers to C that outlive the call
  • โœ“ Don't store Go pointers in C memory

Build Configuration

Use #cgo directives to configure the C compiler and linker:

Output
Click Run to execute your code
Common #cgo Directives:
// Compiler flags
#cgo CFLAGS: -Wall -O2 -I/usr/local/include

// Linker flags
#cgo LDFLAGS: -L/usr/local/lib -lmylib

// Platform-specific
#cgo linux CFLAGS: -D_GNU_SOURCE
#cgo darwin LDFLAGS: -framework CoreFoundation
#cgo windows LDFLAGS: -lws2_32

// Package config
#cgo pkg-config: gtk+-3.0

Common Mistakes

1. Forgetting to free C memory

// โŒ Wrong - memory leak
func bad() {
    s := C.CString("hello")
    // Forgot to free!
}

// โœ… Correct - always free
func good() {
    s := C.CString("hello")
    defer C.free(unsafe.Pointer(s))
    // Use s...
}

2. Passing Go pointers incorrectly

// โŒ Wrong - Go pointer stored in C
var globalPtr *C.char
func bad() {
    s := "hello"
    globalPtr = C.CString(s) // Dangerous!
}

// โœ… Correct - manage lifetime properly
func good() {
    s := C.CString("hello")
    defer C.free(unsafe.Pointer(s))
    // Use s within this scope
}

3. Blank line before import "C"

// โŒ Wrong - blank line breaks CGO
/*
#include 
*/

import "C" // Error!

// โœ… Correct - no blank line
/*
#include 
*/
import "C" // Works!

Exercise: C Library Wrapper

Task: Create a Go wrapper for a simple C math library.

Requirements:

  • C functions for basic math operations
  • Go wrapper with proper error handling
  • Memory management
  • Type conversions
Show Solution
package main

/*
#include 
#include 

typedef struct {
    double result;
    int error;
} MathResult;

MathResult safe_divide(double a, double b) {
    MathResult r;
    if (b == 0.0) {
        r.error = 1;
        r.result = 0.0;
    } else {
        r.error = 0;
        r.result = a / b;
    }
    return r;
}

double* create_array(int size) {
    return (double*)malloc(size * sizeof(double));
}

void fill_array(double* arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = sqrt((double)i);
    }
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

// SafeDivide wraps C division with error handling
func SafeDivide(a, b float64) (float64, error) {
    result := C.safe_divide(C.double(a), C.double(b))
    
    if result.error != 0 {
        return 0, fmt.Errorf("division by zero")
    }
    
    return float64(result.result), nil
}

// CalculateSquareRoots creates array of square roots
func CalculateSquareRoots(n int) []float64 {
    // Allocate C array
    cArray := C.create_array(C.int(n))
    defer C.free(unsafe.Pointer(cArray))
    
    // Fill with values
    C.fill_array(cArray, C.int(n))
    
    // Convert to Go slice
    goSlice := make([]float64, n)
    cSlice := (*[1 << 30]C.double)(unsafe.Pointer(cArray))[:n:n]
    
    for i := 0; i < n; i++ {
        goSlice[i] = float64(cSlice[i])
    }
    
    return goSlice
}

func main() {
    // Test division
    result, err := SafeDivide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10 / 2 = %.2f\n", result)
    }
    
    // Test division by zero
    _, err = SafeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    }
    
    // Test array
    roots := CalculateSquareRoots(5)
    fmt.Println("Square roots:", roots)
}

Summary

  • CGO enables Go to call C code
  • import "C" provides access to C functions
  • Type conversions are explicit (C.int, C.CString)
  • Memory management is critical - always free C memory
  • C.CString() allocates, must free with C.free()
  • C.GoString() copies, no free needed
  • //export allows C to call Go functions
  • #cgo directives configure compiler and linker
  • Build tags control conditional compilation
  • Tradeoffs - power vs complexity

What's Next?

You've learned CGO basics! While powerful, CGO should be used judiciously. Next, you'll explore the unsafe package, which provides low-level memory operations and type conversions that bypass Go's type safety - use with extreme caution!