CGO Basics
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.
- Legacy Integration - Use existing C libraries
- Performance - Optimize critical paths with C
- System APIs - Access OS-specific C APIs
- Hardware - Interface with device drivers
- โ ๏ธ 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":
Click Run to execute your code
- C code goes in comment block before
import "C" - No blank line between comment and import
- Prefix C functions with
C. - Use
#includefor C headers
Type Conversions
Converting between Go and C types requires explicit conversions:
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:
Click Run to execute your code
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:
Click Run to execute your code
- 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:
Click Run to execute your code
- โ 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:
Click Run to execute your code
// 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!
Enjoying these tutorials?