CGo - C Interoperability

Why CGo Matters

Consider building a house and needing a specialized, pre-built kitchen that costs $50,000. You could try to build it yourself from scratch, or you could just connect the plumbing and electricity to the pre-built unit. The kitchen works perfectly—you just need to ensure you don't accidentally flood the house with bad plumbing connections.

Real-World Impact:

SQLite Database Access - Used by millions of applications:

  • Pure Go implementation: Slow, incomplete feature set, 3x slower than C
  • CGo + SQLite C library: Production-grade, 100% compatible, battle-tested
  • Performance: 5-10x faster for complex queries
  • Result: Go apps get enterprise-grade database without rewriting decades of optimization

Docker - Container platform running on millions of servers:

  • Uses CGo for Linux namespaces/cgroups syscalls
  • Network programming via netlink
  • Storage drivers using device-mapper
  • Impact: Without CGo, Docker wouldn't exist—Go would need to reimplement entire Linux syscall interface

High-Frequency Trading Systems - Millions of dollars at stake:

  • Go code handles orchestration and business logic
  • CGo calls hand-tuned C for market data processing
  • Performance: 50ns per C call vs 300ns for Go equivalent
  • Result: Milliseconds of latency saved = millions in trading profits

CGo is Go's bridge to decades of C libraries—OpenSSL, libgit2, libcurl, and millions of lines of battle-tested code. Understanding CGo separates hobby projects from production systems that leverage existing ecosystems.

Learning Objectives

By the end of this article, you will master:

  • CGo Fundamentals: Master the import "C" syntax and build system integration
  • Type Conversion: Safely convert between Go and C data types, including complex structures
  • Memory Management: Prevent memory leaks and handle garbage collection across language boundaries
  • Function Integration: Call C functions from Go and export Go functions to C
  • Production Patterns: Build production-ready wrappers with error handling and thread safety

Core Concepts - Understanding CGo

What is CGo?

CGo is Go's foreign function interface that enables Go programs to call C code and C programs to call Go code. It acts as a translator between two different programming models—Go with its garbage collection and goroutines, and C with its manual memory management and raw pointers.

Real-world Analogy: CGo is like having a skilled translator who speaks both English and Mandarin fluently. When your English-speaking Go team needs to work with a Mandarin-speaking C library, the translator ensures nothing gets lost in communication. However, each translation takes time and effort, so you only use the translator when absolutely necessary.

The Language Boundary

Go's Side:

  • Garbage collected memory
  • Goroutines for concurrency
  • Type safety and interfaces
  • Built-in error handling

C's Side:

  • Manual memory management
  • Threads and pthreads
  • Raw pointers and type casting
  • Return codes and error codes

CGo's Role:

  • Translates calling conventions
  • Converts data types between languages
  • Handles memory ownership transfer
  • Provides bridge for function calls

Why Use CGo?

Good Use Cases:

  • Existing C libraries: Leverage battle-tested code without rewriting
  • System APIs: Access low-level OS features not available in Go
  • Performance optimization: Use hand-tuned C code for critical paths
  • Hardware interfaces: Interact with specialized hardware drivers

When to Avoid:

  • Pure Go alternatives: Go crypto/tls vs OpenSSL
  • Simple tasks: Cgo adds ~50-100ns overhead per call
  • Cross-compilation priority: CGo complicates cross-platform builds
  • Simplicity needs: CGo adds complexity to build and debugging

Practical Examples - Hands-On CGo

Example 1: Basic CGo Setup and Function Calls

Let's start with the foundation: calling simple C functions from Go and understanding the basic setup.

 1//go:build cgo
 2// +build cgo
 3
 4package main
 5
 6/*
 7#include <stdio.h>
 8#include <stdlib.h>
 9
10// Simple C functions to demonstrate CGo
11int add_numbers(int a, int b) {
12    return a + b;
13}
14
15void print_message(const char* message) {
16    printf("From C: %s\n", message);
17}
18
19char* create_string() {
20    // Allocate memory that Go must free
21    char* result = malloc(20 * sizeof(char));
22    if {
23        strcpy(result, "Hello from C memory!");
24    }
25    return result;
26}
27*/
28import "C"
29import (
30    "fmt"
31    "unsafe"
32)
33
34func main() {
35    fmt.Println("=== Basic CGo Examples ===")
36
37    // Example 1: Simple integer operations
38    result := C.add_numbers(10, 20)
39    fmt.Printf("C.add_numbers(10, 20) = %d\n", result)
40
41    // Example 2: String handling
42    message := C.CString("Hello from Go!")
43    defer C.free(unsafe.Pointer(message)) // CRITICAL: Always free C memory!
44
45    C.print_message(message)
46
47    // Example 3: Memory allocation and transfer
48    cString := C.create_string()
49    defer C.free(unsafe.Pointer(cString)) // CRITICAL: Free C-allocated memory
50
51    goString := C.GoString(cString)
52    fmt.Printf("C string converted to Go: %s\n", goString)
53}

What's happening:

  1. Preamble: The /* ... */ comment contains C code that CGo compiles separately
  2. Import: import "C" creates the bridge pseudo-package
  3. Type conversion: C.int, C.char* map between Go and C types
  4. Memory management: C.CString() allocates C memory, C.free() deallocates it

Example 2: String and Slice Operations

Strings are the most complex data type to handle across the Go/C boundary due to different memory models.

  1//go:build cgo
  2// +build cgo
  3
  4package main
  5
  6/*
  7#include <stdio.h>
  8#include <stdlib.h>
  9#include <string.h>
 10
 11// Process a Go string in C
 12int process_string(const char* input, char* output, int output_size) {
 13    if {
 14        return -1; // Error: NULL pointer
 15    }
 16
 17    int input_len = strlen(input);
 18    if {
 19        return -2; // Error: empty string
 20    }
 21
 22    // Simple processing: convert to uppercase and reverse
 23    int i, j = 0;
 24    for {
 25        if {
 26            return -3; // Error: output buffer too small
 27        }
 28        output[j] = toupper(input[i]);
 29    }
 30    output[j] = '\0'; // Null-terminate
 31
 32    return j; // Return number of characters processed
 33}
 34
 35// Process Go slice of integers
 36int sum_array(int* array, int size) {
 37    if {
 38        return 0;
 39    }
 40
 41    int sum = 0;
 42    for {
 43        sum += array[i];
 44    }
 45    return sum;
 46}
 47*/
 48import "C"
 49import (
 50    "fmt"
 51    "unsafe"
 52)
 53
 54// Wrapper for safe string processing
 55func ProcessString(input string) {
 56    if len(input) == 0 {
 57        return "", fmt.Errorf("empty input string")
 58    }
 59
 60    // Convert Go string to C string
 61    cInput := C.CString(input)
 62    defer C.free(unsafe.Pointer(cInput))
 63
 64    // Allocate output buffer in C
 65    outputSize := len(input) + 1 // +1 for null terminator
 66    cOutput := C.malloc(C.size_t(outputSize))
 67    if cOutput == nil {
 68        return "", fmt.Errorf("failed to allocate C memory")
 69    }
 70    defer C.free(cOutput)
 71
 72    // Call C function
 73    result := C.process_string(cInput,(cOutput), C.int(outputSize))
 74    if result < 0 {
 75        return "", fmt.Errorf("C processing failed with code: %d", result)
 76    }
 77
 78    // Convert C result back to Go string
 79    return C.GoString((*C.char)(cOutput)), nil
 80}
 81
 82// Wrapper for safe slice processing
 83func SumArray(numbers []int32) {
 84    if len(numbers) == 0 {
 85        return 0, fmt.Errorf("empty array")
 86    }
 87
 88    // Get pointer to Go slice data
 89    ptr := unsafe.Pointer(&numbers[0])
 90    cArray :=(ptr)
 91
 92    // Call C function
 93    sum := C.sum_array(cArray, C.int(len(numbers)))
 94
 95    return int32(sum), nil
 96}
 97
 98func main() {
 99    fmt.Println("=== String and Slice Operations ===")
100
101    // Example 1: String processing
102    input := "Hello CGo!"
103    result, err := ProcessString(input)
104    if err != nil {
105        fmt.Printf("Error processing string: %v\n", err)
106    } else {
107        fmt.Printf("Input: %q -> Processed: %q\n", input, result)
108    }
109
110    // Example 2: Slice processing
111    numbers := []int32{1, 2, 3, 4, 5, 10}
112    sum, err := SumArray(numbers)
113    if err != nil {
114        fmt.Printf("Error summing array: %v\n", err)
115    } else {
116        fmt.Printf("Array %v -> Sum: %d\n", numbers, sum)
117    }
118}

Example 3: Working with Structs

Complex data structures require careful handling of memory layout and field alignment.

  1//go:build cgo
  2// +build cgo
  3
  4package main
  5
  6/*
  7#include <stdio.h>
  8#include <stdlib.h>
  9#include <string.h>
 10
 11// C structure definition
 12typedef struct {
 13    int id;
 14    char name[50];
 15    double salary;
 16    int is_active;
 17} Employee;
 18
 19// Create an employee in C memory
 20Employee* create_employee(int id, const char* name, double salary) {
 21    Employee* emp = malloc(sizeof(Employee));
 22    if {
 23        return NULL;
 24    }
 25
 26    emp->id = id;
 27    strncpy(emp->name, name, sizeof(emp->name) - 1);
 28    emp->name[sizeof(emp->name) - 1] = '\0';
 29    emp->salary = salary;
 30    emp->is_active = 1;
 31
 32    return emp;
 33}
 34
 35// Print employee information
 36void print_employee(Employee* emp) {
 37    if {
 38        printf("Employee pointer is NULL\n");
 39        return;
 40    }
 41
 42    printf("Employee { id: %d, name: %s, salary: %.2f, active: %s }\n",
 43           emp->id, emp->name, emp->salary, emp->is_active ? "yes" : "no");
 44}
 45
 46// Free employee memory
 47void free_employee(Employee* emp) {
 48    if {
 49        free(emp);
 50    }
 51}
 52
 53// Update employee salary
 54void update_salary(Employee* emp, double new_salary) {
 55    if {
 56        emp->salary = new_salary;
 57    }
 58}
 59*/
 60import "C"
 61import (
 62    "fmt"
 63    "unsafe"
 64)
 65
 66// Go equivalent of C struct for safer handling
 67type Employee struct {
 68    ID       int32
 69    Name     string
 70    Salary   float64
 71    IsActive bool
 72    cPointer *C.Employee // Keep reference to C memory
 73}
 74
 75// Create employee in Go, allocating C memory
 76func NewEmployee(id int32, name string, salary float64) {
 77    // Convert Go string to C string
 78    cName := C.CString(name)
 79    defer C.free(unsafe.Pointer(cName))
 80
 81    // Create employee in C memory
 82    cEmp := C.create_employee(C.int(id), cName, C.double(salary))
 83    if cEmp == nil {
 84        return nil, fmt.Errorf("failed to create C employee")
 85    }
 86
 87    // Wrap in Go struct for safer handling
 88    return &Employee{
 89        ID:       id,
 90        Name:     name,
 91        Salary:   salary,
 92        IsActive: true,
 93        cPointer: cEmp,
 94    }, nil
 95}
 96
 97// Print employee information
 98func Print() {
 99    if e == nil || e.cPointer == nil {
100        fmt.Println("Employee is nil")
101        return
102    }
103
104    // Call C function to print
105    C.print_employee(e.cPointer)
106
107    // Print Go version
108    fmt.Printf("Go Employee { ID: %d, Name: %s, Salary: %.2f, Active: %t }\n",
109        e.ID, e.Name, e.Salary, e.IsActive)
110}
111
112// Update salary safely
113func UpdateSalary(newSalary float64) error {
114    if e == nil || e.cPointer == nil {
115        return fmt.Errorf("employee is nil")
116    }
117
118    e.Salary = newSalary
119    C.update_salary(e.cPointer, C.double(newSalary))
120
121    return nil
122}
123
124// Clean up C memory
125func Close() error {
126    if e == nil || e.cPointer == nil {
127        return fmt.Errorf("employee is nil or already closed")
128    }
129
130    C.free_employee(e.cPointer)
131    e.cPointer = nil
132
133    return nil
134}
135
136func main() {
137    fmt.Println("=== Struct Operations ===")
138
139    // Example 1: Create and manage employee
140    emp, err := NewEmployee(1001, "John Doe", 75000.50)
141    if err != nil {
142        fmt.Printf("Error creating employee: %v\n", err)
143        return
144    }
145
146    defer emp.Close() // Ensure cleanup
147
148    emp.Print()
149
150    // Example 2: Update employee
151    fmt.Println("\nUpdating salary...")
152    err = emp.UpdateSalary(85000.00)
153    if err != nil {
154        fmt.Printf("Error updating salary: %v\n", err)
155        return
156    }
157
158    emp.Print()
159}

Example 4: Exporting Go Functions to C

Sometimes C code needs to call back into Go functions—for event handling, callbacks, or exposing Go libraries to other languages.

  1//go:build cgo
  2// +build cgo
  3
  4package main
  5
  6/*
  7#include <stdio.h>
  8#include <stdlib.h>
  9
 10// Function pointer types for callbacks
 11typedef int(int, int);
 12typedef void(const char*);
 13
 14// C function that accepts a Go callback
 15int perform_operation(int a, int b, MathOperation op) {
 16    if {
 17        return -1;
 18    }
 19    return op(a, b);
 20}
 21
 22// C function that processes strings with a Go callback
 23void process_strings(const char** strings, int count, StringProcessor processor) {
 24    if {
 25        return;
 26    }
 27
 28    for {
 29        processor(strings[i]);
 30    }
 31}
 32
 33// Utility function to create test string array
 34const char** create_string_array() {
 35    static const char* strings[] = {
 36        "Hello from C",
 37        "Processing in Go",
 38        "Callback demonstration",
 39        NULL // Terminator
 40    };
 41    return strings;
 42}
 43*/
 44import "C"
 45import (
 46    "fmt"
 47    "sync"
 48    "unsafe"
 49)
 50
 51var (
 52    callbackCount int
 53    callbackMutex sync.Mutex
 54)
 55
 56// Export Go function to be called from C
 57//export GoAdd
 58func GoAdd(a, b C.int) C.int {
 59    callbackMutex.Lock()
 60    callbackCount++
 61    currentCount := callbackCount
 62    callbackMutex.Unlock()
 63
 64    result := int(a + b)
 65    fmt.Printf("GoAdd(%d, %d) = %d\n", a, b, result, currentCount)
 66    return C.int(result)
 67}
 68
 69// Export Go function to be called from C
 70//export GoMultiply
 71func GoMultiply(a, b C.int) C.int {
 72    callbackMutex.Lock()
 73    callbackCount++
 74    currentCount := callbackCount
 75    callbackMutex.Unlock()
 76
 77    result := int(a * b)
 78    fmt.Printf("GoMultiply(%d, %d) = %d\n", a, b, result, currentCount)
 79    return C.int(result)
 80}
 81
 82// Export Go function for string processing
 83//export GoProcessString
 84func GoProcessString(cString *C.char) {
 85    if cString == nil {
 86        return
 87    }
 88
 89    callbackMutex.Lock()
 90    callbackCount++
 91    currentCount := callbackCount
 92    callbackMutex.Unlock()
 93
 94    goString := C.GoString(cString)
 95    processed := fmt.Sprintf("Processed: %s", goString, currentCount)
 96    fmt.Printf("Go received and processed: %s\n", processed)
 97}
 98
 99func main() {
100    fmt.Println("=== Go Functions Exported to C ===")
101
102    // Example 1: Using Go math functions from C
103    result1 := C.perform_operation(10, 20,(unsafe.Pointer(C.GoAdd)))
104    fmt.Printf("C called GoAdd(10, 20) = %d\n", result1)
105
106    result2 := C.perform_operation(5, 6,(unsafe.Pointer(C.GoMultiply)))
107    fmt.Printf("C called GoMultiply(5, 6) = %d\n", result2)
108
109    // Example 2: String processing with Go callback
110    cStrings := C.create_string_array()
111    C.process_strings(cStrings, C.int(3),(unsafe.Pointer(C.GoProcessString)))
112
113    fmt.Printf("Total callbacks made: %d\n", callbackCount)
114}

Example 5: Production-Ready C Library Wrapper

Let's create a complete, production-ready wrapper for a hypothetical C encryption library.

  1//go:build cgo
  2// +build cgo
  3
  4package encryption
  5
  6/*
  7#cgo LDFLAGS: -L. -lencryption -lcrypto
  8#cgo CFLAGS: -I.
  9#include <encryption.h>
 10#include <stdlib.h>
 11#include <string.h>
 12*/
 13import "C"
 14import (
 15    "errors"
 16    "fmt"
 17    "runtime"
 18    "sync"
 19    "time"
 20    "unsafe"
 21)
 22
 23// EncryptionContext wraps C encryption context
 24type EncryptionContext struct {
 25    ctx       *C.EncryptionContext
 26    mu        sync.RWMutex
 27    algorithm string
 28    keySize   int
 29}
 30
 31// EncryptionError represents encryption-specific errors
 32type EncryptionError struct {
 33    Code    int
 34    Message string
 35}
 36
 37func Error() string {
 38    return fmt.Sprintf("encryption error %d: %s", e.Code, e.Message)
 39}
 40
 41// Constants for encryption algorithms
 42const (
 43    AlgorithmAES256  = "aes-256"
 44    AlgorithmChaCha20 = "chacha20"
 45    AlgorithmRSA2048  = "rsa-2048"
 46)
 47
 48// Error codes from C library
 49const (
 50    ErrSuccess        = 0
 51    ErrInvalidKey    = -1
 52    ErrInvalidData   = -2
 53    ErrBufferTooSmall = -3
 54    ErrMemoryError   = -4
 55)
 56
 57// NewEncryptionContext creates a new encryption context
 58func NewEncryptionContext(algorithm string, keySize int) {
 59    // Validate parameters
 60    if !isValidAlgorithm(algorithm) {
 61        return nil, &EncryptionError{
 62            Code:    ErrInvalidKey,
 63            Message: fmt.Sprintf("unsupported algorithm: %s", algorithm),
 64        }
 65    }
 66
 67    if !isValidKeySize(algorithm, keySize) {
 68        return nil, &EncryptionError{
 69            Code:    ErrInvalidKey,
 70            Message: fmt.Sprintf("invalid key size %d for algorithm %s", keySize, algorithm),
 71        }
 72    }
 73
 74    // Create C context
 75    cAlgorithm := C.CString(algorithm)
 76    defer C.free(unsafe.Pointer(cAlgorithm))
 77
 78    ctx := C.encryption_init(cAlgorithm, C.int(keySize))
 79    if ctx == nil {
 80        return nil, &EncryptionError{
 81            Code:    ErrMemoryError,
 82            Message: "failed to create encryption context",
 83        }
 84    }
 85
 86    goCtx := &EncryptionContext{
 87        ctx:       ctx,
 88        algorithm: algorithm,
 89        keySize:   keySize,
 90    }
 91
 92    // Set finalizer for cleanup
 93    runtime.SetFinalizer(goCtx,.Close)
 94
 95    return goCtx, nil
 96}
 97
 98// Encrypt encrypts data using the configured algorithm
 99func Encrypt(plaintext []byte) {
100    ec.mu.RLock()
101    defer ec.mu.RUnlock()
102
103    if ec.ctx == nil {
104        return nil, &EncryptionError{
105            Code:    ErrInvalidData,
106            Message: "encryption context is closed",
107        }
108    }
109
110    if len(plaintext) == 0 {
111        return nil, nil // Empty plaintext results in empty ciphertext
112    }
113
114    // Get output buffer size
115    cipherSize := C.encryption_get_cipher_size(ec.ctx, C.size_t(len(plaintext)))
116    if cipherSize <= 0 {
117        return nil, &EncryptionError{
118            Code:    ErrBufferTooSmall,
119            Message: "failed to calculate cipher size",
120        }
121    }
122
123    // Allocate output buffer
124    ciphertext := make([]byte, cipherSize)
125
126    // Perform encryption
127    result := C.encryption_encrypt(
128        ec.ctx,
129       (unsafe.Pointer(&plaintext[0])),
130        C.size_t(len(plaintext)),
131       (unsafe.Pointer(&ciphertext[0])),
132        &cipherSize,
133    )
134
135    if result != ErrSuccess {
136        return nil, &EncryptionError{
137            Code:    int(result),
138            Message: fmt.Sprintf("encryption failed with code %d", result),
139        }
140    }
141
142    // Truncate to actual size
143    return ciphertext[:cipherSize], nil
144}
145
146// Decrypt decrypts data using the configured algorithm
147func Decrypt(ciphertext []byte) {
148    ec.mu.RLock()
149    defer ec.mu.RUnlock()
150
151    if ec.ctx == nil {
152        return nil, &EncryptionError{
153            Code:    ErrInvalidData,
154            Message: "encryption context is closed",
155        }
156    }
157
158    if len(ciphertext) == 0 {
159        return nil, nil // Empty ciphertext results in empty plaintext
160    }
161
162    // Get output buffer size
163    plainSize := C.encryption_get_plain_size(ec.ctx, C.size_t(len(ciphertext)))
164    if plainSize <= 0 {
165        return nil, &EncryptionError{
166            Code:    ErrBufferTooSmall,
167            Message: "failed to calculate plain size",
168        }
169    }
170
171    // Allocate output buffer
172    plaintext := make([]byte, plainSize)
173
174    // Perform decryption
175    result := C.encryption_decrypt(
176        ec.ctx,
177       (unsafe.Pointer(&ciphertext[0])),
178        C.size_t(len(ciphertext)),
179       (unsafe.Pointer(&plaintext[0])),
180        &plainSize,
181    )
182
183    if result != ErrSuccess {
184        return nil, &EncryptionError{
185            Code:    int(result),
186            Message: fmt.Sprintf("decryption failed with code %d", result),
187        }
188    }
189
190    // Truncate to actual size
191    return plaintext[:plainSize], nil
192}
193
194// GenerateKey generates a new encryption key
195func GenerateKey() {
196    ec.mu.RLock()
197    defer ec.mu.RUnlock()
198
199    if ec.ctx == nil {
200        return nil, &EncryptionError{
201            Code:    ErrInvalidData,
202            Message: "encryption context is closed",
203        }
204    }
205
206    keySize := C.size_t(ec.keySize)
207    key := make([]byte, keySize)
208
209    result := C.encryption_generate_key(ec.ctx,(unsafe.Pointer(&key[0])))
210    if result != ErrSuccess {
211        return nil, &EncryptionError{
212            Code:    int(result),
213            Message: fmt.Sprintf("key generation failed with code %d", result),
214        }
215    }
216
217    return key, nil
218}
219
220// Close cleans up the C encryption context
221func Close() error {
222    ec.mu.Lock()
223    defer ec.mu.Unlock()
224
225    if ec.ctx == nil {
226        return nil // Already closed
227    }
228
229    C.encryption_cleanup(ec.ctx)
230    ec.ctx = nil
231
232    return nil
233}
234
235// GetAlgorithm returns the configured algorithm
236func GetAlgorithm() string {
237    ec.mu.RLock()
238    defer ec.mu.RUnlock()
239    return ec.algorithm
240}
241
242// GetKeySize returns the configured key size
243func GetKeySize() int {
244    ec.mu.RLock()
245    defer ec.mu.RUnlock()
246    return ec.keySize
247}
248
249// Helper functions
250func isValidAlgorithm(algorithm string) bool {
251    switch algorithm {
252    case AlgorithmAES256, AlgorithmChaCha20, AlgorithmRSA2048:
253        return true
254    default:
255        return false
256    }
257}
258
259func isValidKeySize(algorithm string, keySize int) bool {
260    switch algorithm {
261    case AlgorithmAES256:
262        return keySize == 32
263    case AlgorithmChaCha20:
264        return keySize == 32
265    case AlgorithmRSA2048:
266        return keySize == 256
267    default:
268        return false
269    }
270}
271
272// GetVersion returns the encryption library version
273func GetVersion() string {
274    cVersion := C.encryption_get_version()
275    if cVersion == nil {
276        return "unknown"
277    }
278    defer C.free(unsafe.Pointer(cVersion))
279
280    return C.GoString(cVersion)
281}
282
283// EncryptionStats provides performance statistics
284type EncryptionStats struct {
285    EncryptedBytes   uint64
286    DecryptedBytes   uint64
287    OperationsCount  uint64
288    AverageLatency   time.Duration
289    ErrorCount       uint64
290    LastOperation    time.Time
291}
292
293var stats EncryptionStats
294var statsMutex sync.RWMutex
295
296// UpdateStats updates encryption statistics
297func UpdateStats(bytesProcessed uint64, latency time.Duration, isError bool) {
298    statsMutex.Lock()
299    defer statsMutex.Unlock()
300
301    if isError {
302        stats.ErrorCount++
303    } else {
304        stats.EncryptedBytes += bytesProcessed
305        stats.OperationsCount++
306        stats.AverageLatency = time.Duration(
307            + uint64(latency)) / 2,
308        )
309    }
310
311    stats.LastOperation = time.Now()
312}
313
314// GetStats returns current statistics
315func GetStats() EncryptionStats {
316    statsMutex.RLock()
317    defer statsMutex.RUnlock()
318    return stats
319}
320
321// ResetStats resets all statistics
322func ResetStats() {
323    statsMutex.Lock()
324    defer statsMutex.Unlock()
325
326    stats = EncryptionStats{}
327}

Common Patterns and Pitfalls

Pattern 1: Safe Memory Management

Memory management is the most critical aspect of CGo. Always follow strict ownership rules.

  1//go:build cgo
  2// +build cgo
  3
  4package memory
  5
  6/*
  7#include <stdlib.h>
  8#include <string.h>
  9
 10// C function that allocates memory for caller to free
 11char* create_dynamic_string(const char* prefix) {
 12    if {
 13        return NULL;
 14    }
 15
 16    int prefix_len = strlen(prefix);
 17    char* result = malloc(prefix_len + 20); // " - processed" + null
 18    if {
 19        return NULL;
 20    }
 21
 22    strcpy(result, prefix);
 23    strcat(result, " - processed");
 24
 25    return result;
 26}
 27*/
 28import "C"
 29import (
 30    "fmt"
 31    "runtime"
 32    "unsafe"
 33)
 34
 35// Safe wrapper with automatic cleanup
 36type CString struct {
 37    ptr *C.char
 38}
 39
 40// NewCString creates a managed C string
 41func NewCString(s string) *CString {
 42    return &CString{
 43        ptr: C.CString(s),
 44    }
 45}
 46
 47// String returns the Go string value
 48func String() string {
 49    if cs.ptr == nil {
 50        return ""
 51    }
 52    return C.GoString(cs.ptr)
 53}
 54
 55// Free releases the C memory
 56func Free() {
 57    if cs.ptr != nil {
 58        C.free(unsafe.Pointer(cs.ptr))
 59        cs.ptr = nil
 60    }
 61}
 62
 63// GetPointer returns the raw C pointer
 64func GetPointer() *C.char {
 65    return cs.ptr
 66}
 67
 68// ProcessString demonstrates safe memory management
 69func ProcessString(input string) {
 70    // Create managed C string
 71    cInput := NewCString(input)
 72    defer cInput.Free()
 73
 74    // Call C function that allocates memory for us to free
 75    cResult := C.create_dynamic_string(cInput.GetPointer())
 76    if cResult == nil {
 77        return "", fmt.Errorf("C function returned NULL")
 78    }
 79
 80    // Create managed wrapper for C-allocated memory
 81    managedResult := &CString{ptr: cResult}
 82    defer managedResult.Free()
 83
 84    return managedResult.String(), nil
 85}
 86
 87func main() {
 88    fmt.Println("=== Safe Memory Management ===")
 89
 90    result, err := ProcessString("Test")
 91    if err != nil {
 92        fmt.Printf("Error: %v\n", err)
 93        return
 94    }
 95
 96    fmt.Printf("Processed result: %s\n", result)
 97
 98    // Demonstrate automatic cleanup with finalizer
 99    cs := NewCString("Finalizer test")
100    runtime.SetFinalizer(cs, func(obj interface{}) {
101        if cs, ok := obj.(*CString); ok {
102            fmt.Println("Finalizer: Cleaning up C memory")
103            cs.Free()
104        }
105    })
106
107    fmt.Printf("String from finalizer: %s\n", cs.String())
108}

Pattern 2: Error Handling Across Languages

Go and C have different error handling paradigms—Go uses multiple returns, C uses return codes and errno.

  1//go:build cgo
  2// +build cgo
  3
  4package errorhandling
  5
  6/*
  7#include <errno.h>
  8#include <string.h>
  9
 10// C function that demonstrates different error types
 11int c_operation_with_errors(int operation, char* buffer, int buffer_size) {
 12    switch {
 13        case 0: // Success
 14            strcpy(buffer, "Success");
 15            return 0;
 16
 17        case 1: // Invalid parameter
 18            errno = EINVAL;
 19            return -1;
 20
 21        case 2: // Buffer too small
 22            errno = ENOSPC;
 23            return -2;
 24
 25        case 3: // Permission denied
 26            errno = EACCES;
 27            return -3;
 28
 29        default: // Unknown error
 30            errno = EFAULT;
 31            return -4;
 32    }
 33}
 34
 35// Get human-readable error message
 36const char* get_error_string(int error_code) {
 37    static char buffer[256];
 38
 39    switch {
 40        case 0:
 41            strcpy(buffer, "Operation successful");
 42            break;
 43        case -1:
 44            strcpy(buffer, "Invalid parameter provided");
 45            break;
 46        case -2:
 47            strcpy(buffer, "Buffer too small");
 48            break;
 49        case -3:
 50            strcpy(buffer, "Permission denied");
 51            break;
 52        default:
 53            strerror_r(error_code, buffer, sizeof(buffer));
 54            break;
 55    }
 56
 57    return buffer;
 58}
 59*/
 60import "C"
 61import (
 62    "errors"
 63    "fmt"
 64    "runtime"
 65    "syscall"
 66    "unsafe"
 67)
 68
 69// CGoError represents errors from C operations
 70type CGoError struct {
 71    CErrorCode C.int
 72    GoError    error
 73    Message    string
 74}
 75
 76func Error() string {
 77    if e.GoError != nil {
 78        return fmt.Sprintf("CGo error %d: %s", e.CErrorCode, e.Message, e.GoError)
 79    }
 80    return fmt.Sprintf("CGo error %d: %s", e.CErrorCode, e.Message)
 81}
 82
 83func Unwrap() error {
 84    return e.GoError
 85}
 86
 87// Error code constants
 88const (
 89    ErrSuccess      = 0
 90    ErrInvalidParam  = -1
 91    ErrBufferSmall  = -2
 92    ErrPermission   = -3
 93    ErrUnknown     = -4
 94)
 95
 96// Operation represents different C operations
 97type Operation int
 98
 99const (
100    OpSuccess Operation = iota
101    OpInvalidParam
102    OpBufferSmall
103    OpPermission
104)
105
106// PerformCOperation executes a C operation with proper error handling
107func PerformCOperation(op Operation, input string) {
108    if len(input) > 255 {
109        return "", &CGoError{
110            CErrorCode: ErrInvalidParam,
111            Message:    "input string too long",
112        }
113    }
114
115    // Create C buffer
116    buffer := make([]byte, 256)
117    cBuffer :=(unsafe.Pointer(&buffer[0]))
118
119    // Convert input to C string
120    cInput := C.CString(input)
121    defer C.free(unsafe.Pointer(cInput))
122
123    // Call C function
124    result := C.c_operation_with_errors(C.int(op), cBuffer, C.int(len(buffer)))
125
126    if result == ErrSuccess {
127        // Convert C buffer to Go string
128        return C.GoStringN(cBuffer, C.int(256)), nil
129    }
130
131    // Get error message from C
132    cErrorMsg := C.get_error_string(result)
133    if cErrorMsg == nil {
134        return "", &CGoError{
135            CErrorCode: result,
136            Message:    "unknown error",
137        }
138    }
139
140    errorMsg := C.GoString(cErrorMsg)
141
142    // Try to map to Go standard error
143    var goErr error
144    switch result {
145    case ErrInvalidParam:
146        goErr = syscall.EINVAL
147    case ErrBufferSmall:
148        goErr = syscall.ENOSPC
149    case ErrPermission:
150        goErr = syscall.EACCES
151    default:
152        goErr = syscall.EFAULT
153    }
154
155    return "", &CGoError{
156        CErrorCode: result,
157        GoError:    goErr,
158        Message:    errorMsg,
159    }
160}
161
162// Demonstrate error recovery and retry logic
163func PerformWithRetry(operation Operation, input string, maxRetries int) {
164    var lastError error
165
166    for attempt := 0; attempt < maxRetries; attempt++ {
167        result, err := PerformCOperation(operation, input)
168        if err == nil {
169            return result, nil
170        }
171
172        lastError = err
173
174        // Check if error is retryable
175        if cgoErr, ok := err.(*CGoError); ok {
176            switch cgoErr.CErrorCode {
177            case ErrInvalidParam, ErrPermission:
178                // These errors are not retryable
179                return "", fmt.Errorf("non-retryable error on attempt %d: %w", attempt+1, err)
180            case ErrBufferSmall:
181                // This might be retryable with larger buffer
182                runtime.GC() // Give GC a chance to run
183                time.Sleep(time.Millisecond * 100)
184                continue
185            }
186        }
187
188        // Default retry delay
189        time.Sleep(time.Millisecond * 200)
190    }
191
192    return "", fmt.Errorf("operation failed after %d attempts, last error: %w", maxRetries, lastError)
193}
194
195func main() {
196    fmt.Println("=== Error Handling Examples ===")
197
198    // Example 1: Successful operation
199    result, err := PerformCOperation(OpSuccess, "test input")
200    if err != nil {
201        fmt.Printf("Error in successful case: %v\n", err)
202    } else {
203        fmt.Printf("Success result: %s\n", result)
204    }
205
206    // Example 2: Invalid parameter error
207    _, err = PerformCOperation(OpInvalidParam, "test input")
208    if err != nil {
209        fmt.Printf("Expected error: %v\n", err)
210
211        // Demonstrate error unwrapping
212        if cgoErr, ok := err.(*CGoError); ok {
213            fmt.Printf("  C error code: %d\n", cgoErr.CErrorCode)
214            fmt.Printf("  C message: %s\n", cgoErr.Message)
215            if cgoErr.GoError != nil {
216                fmt.Printf("  Go equivalent: %v\n", cgoErr.GoError)
217            }
218        }
219    }
220
221    // Example 3: Retry logic
222    fmt.Println("\nTesting retry logic...")
223    result, err = PerformWithRetry(OpBufferSmall, "test input", 3)
224    if err != nil {
225        fmt.Printf("Retry failed: %v\n", err)
226    } else {
227        fmt.Printf("Retry succeeded: %s\n", result)
228    }
229}

Integration and Mastery - Advanced Techniques

Technique 1: Goroutine-Safe C Library Wrapper

Creating thread-safe wrappers for C libraries that weren't designed for concurrent access.

  1//go:build cgo
  2// +build cgo
  3
  4package threadsafe
  5
  6/*
  7#include <pthread.h>
  8#include <stdlib.h>
  9
 10// Thread-safe counter using C mutex
 11typedef struct {
 12    int value;
 13    pthread_mutex_t mutex;
 14} ThreadSafeCounter;
 15
 16// Initialize counter
 17ThreadSafeCounter* create_counter(int initial_value) {
 18    ThreadSafeCounter* counter = malloc(sizeof(ThreadSafeCounter));
 19    if {
 20        return NULL;
 21    }
 22
 23    counter->value = initial_value;
 24    if != 0) {
 25        free(counter);
 26        return NULL;
 27    }
 28
 29    return counter;
 30}
 31
 32// Increment counter
 33int increment_counter(ThreadSafeCounter* counter) {
 34    if {
 35        return -1;
 36    }
 37
 38    pthread_mutex_lock(&counter->mutex);
 39    int result = ++counter->value;
 40    pthread_mutex_unlock(&counter->mutex);
 41
 42    return result;
 43}
 44
 45// Get counter value
 46int get_counter(ThreadSafeCounter* counter) {
 47    if {
 48        return -1;
 49    }
 50
 51    pthread_mutex_lock(&counter->mutex);
 52    int value = counter->value;
 53    pthread_mutex_unlock(&counter->mutex);
 54
 55    return value;
 56}
 57
 58// Cleanup counter
 59void destroy_counter(ThreadSafeCounter* counter) {
 60    if {
 61        pthread_mutex_destroy(&counter->mutex);
 62        free(counter);
 63    }
 64}
 65*/
 66import "C"
 67import (
 68    "fmt"
 69    "runtime"
 70    "sync"
 71    "time"
 72)
 73
 74// ThreadSafeCounter wraps the C counter for safe Go usage
 75type ThreadSafeCounter struct {
 76    cCounter *C.ThreadSafeCounter
 77    mu       sync.RWMutex // Additional Go-level protection
 78}
 79
 80// NewThreadSafeCounter creates a new thread-safe counter
 81func NewThreadSafeCounter(initialValue int) {
 82    cCounter := C.create_counter(C.int(initialValue))
 83    if cCounter == nil {
 84        return nil, fmt.Errorf("failed to create C counter")
 85    }
 86
 87    tsCounter := &ThreadSafeCounter{
 88        cCounter: cCounter,
 89    }
 90
 91    // Set finalizer for cleanup
 92    runtime.SetFinalizer(tsCounter,.Close)
 93
 94    return tsCounter, nil
 95}
 96
 97// Increment atomically increments the counter
 98func Increment() {
 99    tsc.mu.RLock()
100    defer tsc.mu.RUnlock()
101
102    if tsc.cCounter == nil {
103        return 0, fmt.Errorf("counter is closed")
104    }
105
106    result := C.increment_counter(tsc.cCounter)
107    if result < 0 {
108        return 0, fmt.Errorf("failed to increment counter")
109    }
110
111    return int(result), nil
112}
113
114// Get returns the current counter value
115func Get() {
116    tsc.mu.RLock()
117    defer tsc.mu.RUnlock()
118
119    if tsc.cCounter == nil {
120        return 0, fmt.Errorf("counter is closed")
121    }
122
123    result := C.get_counter(tsc.cCounter)
124    if result < 0 {
125        return 0, fmt.Errorf("failed to get counter value")
126    }
127
128    return int(result), nil
129}
130
131// Close cleans up the C counter
132func Close() error {
133    tsc.mu.Lock()
134    defer tsc.mu.Unlock()
135
136    if tsc.cCounter == nil {
137        return nil // Already closed
138    }
139
140    C.destroy_counter(tsc.cCounter)
141    tsc.cCounter = nil
142
143    return nil
144}
145
146// Test concurrent access
147func TestConcurrentAccess() {
148    fmt.Println("=== Testing Concurrent Access ===")
149
150    counter, err := NewThreadSafeCounter(0)
151    if err != nil {
152        fmt.Printf("Error creating counter: %v\n", err)
153        return
154    }
155    defer counter.Close()
156
157    var wg sync.WaitGroup
158    goroutines := 10
159    increments := 1000
160
161    start := time.Now()
162
163    // Launch goroutines to increment counter concurrently
164    for i := 0; i < goroutines; i++ {
165        wg.Add(1)
166        go func(id int) {
167            defer wg.Done()
168
169            for j := 0; j < increments; j++ {
170                _, err := counter.Increment()
171                if err != nil {
172                    fmt.Printf("Goroutine %d: Error incrementing: %v\n", id, err)
173                    return
174                }
175            }
176        }(i)
177    }
178
179    wg.Wait()
180    duration := time.Since(start)
181
182    // Get final value
183    finalValue, err := counter.Get()
184    if err != nil {
185        fmt.Printf("Error getting final value: %v\n", err)
186        return
187    }
188
189    expectedValue := goroutines * increments
190    fmt.Printf("Expected value: %d, Actual value: %d\n", expectedValue, finalValue)
191    fmt.Printf("Correct: %t\n", finalValue == expectedValue)
192    fmt.Printf("Duration: %v\n", duration)
193
194    // Performance stats
195    opsPerSec := float64(goroutines*increments) / duration.Seconds()
196    fmt.Printf("Operations per second: %.0f\n", opsPerSec)
197}
198
199func main() {
200    TestConcurrentAccess()
201}

Summary

Key Takeaways

CGo Essentials:

  1. Memory management is critical - Every C.malloc() needs a corresponding C.free()
  2. Type conversion is bidirectional - Go ↔ C data types need careful handling
  3. Error handling translation - Map C return codes to Go errors appropriately
  4. Thread safety - C libraries often need Go-level synchronization
  5. Performance cost - Each C call has 50-100ns overhead, batch when possible

Critical Patterns:

  • Memory ownership - Clear rules for who frees what and when
  • Safe string handling - Always use C.CString()/C.GoString() with proper cleanup
  • Struct wrapping - Go structs can safely manage C memory lifetimes
  • Function export - Use //export to make Go functions callable from C
  • Error propagation - Translate C error codes to Go error interfaces

CGo Workflow

  1. Design the interface - Plan clean Go API around C library
  2. Implement C wrappers - Create safe Go functions around C functions
  3. Handle memory management - Use defer, finalizers, and clear ownership rules
  4. Add thread safety - Protect C library calls with Go synchronization
  5. Test thoroughly - Test concurrency, error cases, and memory leaks

Commands & Tools

 1# Build with CGo
 2go build
 3
 4# Build without CGo
 5CGO_ENABLED=0 go build
 6
 7# Debug CGo builds
 8go build -gcflags="-m" 2>&1 | grep cgo
 9
10# Show CGo-generated files
11go build -work
12
13# Set C compiler and flags
14CC=clang CGO_CFLAGS="-O3" go build
15
16# Cross-compile with CGo
17GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build

Production Readiness Checklist

  • All C memory allocations have corresponding cleanup
  • Error handling covers all C return codes
  • Thread safety implemented for concurrent access
  • Memory ownership rules clearly documented
  • Resource cleanup uses defer and finalizers
  • Performance benchmarks show acceptable overhead
  • Cross-compilation tested on target platforms
  • Integration tests cover real-world usage scenarios

Next Steps in Your Go Journey

For Systems Programming:

  • Study operating system interfaces and system calls
  • Learn about kernel programming and device drivers
  • Master embedded systems and IoT development
  • Explore hardware abstraction layers

For High-Performance Applications:

  • Study SIMD optimizations and CPU-specific features
  • Learn about GPU programming through CGo
  • Master network programming and protocol stacks
  • Explore scientific computing and data processing

For Library Development:

  • Study API design patterns across language boundaries
  • Learn about versioning and ABI compatibility
  • Master documentation and cross-language examples
  • Explore packaging and distribution strategies

Advanced CGo Patterns

Building a Production-Quality C Wrapper

When wrapping C libraries, you need more than just basic bindings. Production wrappers handle errors, manage resources, and provide Go-idiomatic APIs:

 1package sqlite
 2
 3// #cgo LDFLAGS: -lsqlite3
 4// #include <sqlite3.h>
 5import "C"
 6
 7import (
 8	"fmt"
 9	"unsafe"
10)
11
12// Database wraps a SQLite database connection
13type Database struct {
14	conn *C.sqlite3
15}
16
17// Open opens a database file
18func Open(filename string) (*Database, error) {
19	cFilename := C.CString(filename)
20	defer C.free(unsafe.Pointer(cFilename))
21
22	var conn *C.sqlite3
23	rc := C.sqlite3_open(cFilename, &conn)
24	if rc != C.SQLITE_OK {
25		msg := C.GoString(C.sqlite3_errmsg(conn))
26		return nil, fmt.Errorf("sqlite3_open failed: %s", msg)
27	}
28
29	return &Database{conn: conn}, nil
30}
31
32// Close closes the database connection
33func (db *Database) Close() error {
34	rc := C.sqlite3_close(db.conn)
35	if rc != C.SQLITE_OK {
36		return fmt.Errorf("sqlite3_close failed: code %d", rc)
37	}
38	return nil
39}
40
41// Execute runs a SQL statement
42func (db *Database) Execute(sql string) error {
43	cSQL := C.CString(sql)
44	defer C.free(unsafe.Pointer(cSQL))
45
46	var errmsg *C.char
47	rc := C.sqlite3_exec(db.conn, cSQL, nil, nil, &errmsg)
48	if rc != C.SQLITE_OK {
49		defer C.sqlite3_free(unsafe.Pointer(errmsg))
50		msg := C.GoString(errmsg)
51		return fmt.Errorf("sqlite3_exec failed: %s", msg)
52	}
53
54	return nil
55}
56
57// Query executes a query and returns results
58func (db *Database) Query(sql string) ([]map[string]string, error) {
59	cSQL := C.CString(sql)
60	defer C.free(unsafe.Pointer(cSQL))
61
62	var stmt *C.sqlite3_stmt
63	rc := C.sqlite3_prepare_v2(db.conn, cSQL, -1, &stmt, nil)
64	if rc != C.SQLITE_OK {
65		return nil, fmt.Errorf("sqlite3_prepare_v2 failed")
66	}
67	defer C.sqlite3_finalize(stmt)
68
69	results := make([]map[string]string, 0)
70
71	for C.sqlite3_step(stmt) == C.SQLITE_ROW {
72		row := make(map[string]string)
73		colCount := int(C.sqlite3_column_count(stmt))
74
75		for i := 0; i < colCount; i++ {
76			colName := C.GoString(C.sqlite3_column_name(stmt, C.int(i)))
77			colValue := C.GoString(C.sqlite3_column_text(stmt, C.int(i)))
78			row[colName] = colValue
79		}
80
81		results = append(results, row)
82	}
83
84	return results, nil
85}

CGo and Goroutines - Thread Safety

One of the trickiest aspects of CGo is using C functions from multiple goroutines. The Go runtime uses OS-level threads, but C libraries may not be thread-safe:

 1package thread_safe_wrapper
 2
 3// #cgo LDFLAGS: -lpthread
 4// #include <pthread.h>
 5// #include <stdlib.h>
 6import "C"
 7
 8import (
 9	"fmt"
10	"sync"
11	"unsafe"
12)
13
14// ThreadSafeLib wraps a C library that isn't thread-safe
15type ThreadSafeLib struct {
16	mu sync.Mutex
17}
18
19// NewThreadSafeLib creates a new wrapper
20func NewThreadSafeLib() *ThreadSafeLib {
21	return &ThreadSafeLib{}
22}
23
24// Call wraps unsafe C function calls with mutex protection
25func (t *ThreadSafeLib) Call(fn func() error) error {
26	t.mu.Lock()
27	defer t.mu.Unlock()
28	return fn()
29}
30
31// Example: unsafe C function that needs serialization
32// func (t *ThreadSafeLib) Process(data []byte) (string, error) {
33//     return t.Call(func() error {
34//         cData := C.CBytes(data)
35//         defer C.free(cData)
36//         result := C.process((*C.char)(cData))
37//         return nil
38//     })
39// }

Integration and Mastery - CGo Best Practices

Performance Considerations

CGo calls have overhead. Each call to a C function:

  1. Transitions from Go to C runtime
  2. Marshals arguments (copies data)
  3. Executes the C function
  4. Marshals return values
  5. Transitions back to Go

Minimize this overhead:

❌ Inefficient - Many CGo calls:

1for i := 0; i < 1000000; i++ {
2	result := C.expensive_function() // Called 1M times
3}

✅ Efficient - Batch processing:

1// Process 1M items in a single C call
2cData := C.CBytes(data)
3defer C.free(unsafe.Pointer(cData))
4result := C.expensive_batch_function((*C.char)(cData), C.int(len(data)))

Memory Safety Checklist

  • ✅ Always defer C.free() for allocated C memory
  • ✅ Never keep C pointers beyond function scope
  • ✅ Use C.CString() for Go strings to C
  • ✅ Use C.GoString() for C strings to Go
  • ✅ Check return codes from C functions
  • ✅ Initialize all C structures properly
  • ✅ Be aware of C variable lifetime and scope

Summary - CGo for Production Systems

CGo is your gateway to:

  1. Performance: Access hand-optimized C libraries
  2. Ecosystem: Integrate with decades of existing code
  3. Compatibility: Support platforms with specific C requirements
  4. Specialization: Use domain-specific C libraries (cryptography, graphics, science)

The key to successful CGo development is treating the C boundary as dangerous territory that requires careful navigation. When you respect that boundary—managing memory carefully, checking error codes, and handling threading properly—CGo becomes a powerful tool for building production-grade systems.

Practice Exercises

Exercise 1: Simple C Library Wrapper

Difficulty: Intermediate | Time: 30-40 minutes

Learning Objectives:

  • Understand CGo compilation and linking
  • Master basic C-to-Go type conversion
  • Practice memory management with C memory

Real-World Context:
Wrapping C libraries is a common task when integrating with system utilities or specialized libraries. This exercise teaches you the fundamental patterns used in real production wrappers like go-sqlite3, libgit2 bindings, and network protocol libraries.

Task:
Create a wrapper for a simple C function that calculates string statistics (length, word count, line count). Include proper error handling and memory management.

Solution
 1package main
 2
 3// #include <stdlib.h>
 4// #include <string.h>
 5//
 6// typedef struct {
 7//     int length;
 8//     int word_count;
 9//     int line_count;
10// } StringStats;
11//
12// StringStats get_stats(const char* str) {
13//     StringStats stats = {0};
14//     stats.length = strlen(str);
15//
16//     int in_word = 0;
17//     for (int i = 0; str[i]; i++) {
18//         if (str[i] == '\n') stats.line_count++;
19//         if (str[i] == ' ' || str[i] == '\n' || str[i] == '\t') {
20//             in_word = 0;
21//         } else if (!in_word) {
22//             stats.word_count++;
23//             in_word = 1;
24//         }
25//     }
26//     if (stats.length > 0) stats.line_count++;
27//
28//     return stats;
29// }
30import "C"
31
32import (
33	"fmt"
34	"unsafe"
35)
36
37type Stats struct {
38	Length    int
39	WordCount int
40	LineCount int
41}
42
43func GetStats(text string) Stats {
44	cText := C.CString(text)
45	defer C.free(unsafe.Pointer(cText))
46
47	cStats := C.get_stats(cText)
48
49	return Stats{
50		Length:    int(cStats.length),
51		WordCount: int(cStats.word_count),
52		LineCount: int(cStats.line_count),
53	}
54}
55
56func main() {
57	text := "Hello world\nThis is a test"
58	stats := GetStats(text)
59	fmt.Printf("Length: %d, Words: %d, Lines: %d\n", stats.Length, stats.WordCount, stats.LineCount)
60}

Exercise 2: Error Handling Across Language Boundaries

Difficulty: Intermediate | Time: 25-30 minutes

Learning Objectives:

  • Master error propagation from C to Go
  • Implement production-grade error handling
  • Handle C error codes and messages

Real-World Context:
Production C libraries return error codes that must be properly translated to Go error types. This exercise teaches patterns used in real database drivers, system interfaces, and cryptographic libraries.

Task:
Create a wrapper for C functions that return error codes, implementing proper Go error handling with descriptive error messages.

Solution
 1package main
 2
 3// #include <errno.h>
 4// #include <string.h>
 5// #include <stdlib.h>
 6//
 7// typedef enum {
 8//     SUCCESS = 0,
 9//     ERROR_INVALID_INPUT = 1,
10//     ERROR_MEMORY = 2,
11//     ERROR_TIMEOUT = 3
12// } ErrorCode;
13//
14// ErrorCode safe_operation(const char* input, char** output) {
15//     if (input == NULL) {
16//         return ERROR_INVALID_INPUT;
17//     }
18//     *output = malloc(strlen(input) + 1);
19//     if (*output == NULL) {
20//         return ERROR_MEMORY;
21//     }
22//     strcpy(*output, input);
23//     return SUCCESS;
24// }
25import "C"
26
27import (
28	"fmt"
29	"unsafe"
30)
31
32type OperationError struct {
33	Code    int
34	Message string
35}
36
37func (e *OperationError) Error() string {
38	return fmt.Sprintf("operation error: %s (code: %d)", e.Message, e.Code)
39}
40
41func SafeOperation(input string) (string, error) {
42	cInput := C.CString(input)
43	defer C.free(unsafe.Pointer(cInput))
44
45	var cOutput *C.char
46	code := C.safe_operation(cInput, &cOutput)
47	defer C.free(unsafe.Pointer(cOutput))
48
49	if code != C.SUCCESS {
50		messages := map[C.ErrorCode]string{
51			C.ERROR_INVALID_INPUT: "invalid input",
52			C.ERROR_MEMORY:        "memory allocation failed",
53			C.ERROR_TIMEOUT:       "operation timed out",
54		}
55		msg := messages[code]
56		return "", &OperationError{Code: int(code), Message: msg}
57	}
58
59	return C.GoString(cOutput), nil
60}
61
62func main() {
63	result, err := SafeOperation("hello")
64	if err != nil {
65		fmt.Println("Error:", err)
66	} else {
67		fmt.Println("Result:", result)
68	}
69}

Exercise 3: Memory Management and Resource Cleanup

Difficulty: Intermediate | Time: 20-25 minutes

Learning Objectives:

  • Master C memory allocation and deallocation
  • Prevent memory leaks with proper cleanup
  • Use defer for resource management

Real-World Context:
Incorrect memory management is the #1 source of bugs in CGo code. This exercise demonstrates patterns used across production libraries for safe resource handling.

Task:
Implement a wrapper that allocates C memory, uses it, and properly cleans up, handling edge cases and errors.

Solution
 1package main
 2
 3// #include <stdlib.h>
 4// #include <string.h>
 5//
 6// char* allocate_buffer(int size) {
 7//     if (size <= 0) return NULL;
 8//     return malloc(size);
 9// }
10//
11// void fill_buffer(char* buf, int size, const char* data) {
12//     strncpy(buf, data, size - 1);
13//     buf[size - 1] = '\0';
14// }
15//
16// void free_buffer(char* buf) {
17//     free(buf);
18// }
19import "C"
20
21import (
22	"fmt"
23	"unsafe"
24)
25
26type Buffer struct {
27	ptr  *C.char
28	size int
29}
30
31func NewBuffer(size int) (*Buffer, error) {
32	if size <= 0 {
33		return nil, fmt.Errorf("invalid size: %d", size)
34	}
35
36	ptr := C.allocate_buffer(C.int(size))
37	if ptr == nil {
38		return nil, fmt.Errorf("allocation failed")
39	}
40
41	return &Buffer{ptr: ptr, size: size}, nil
42}
43
44func (b *Buffer) Fill(data string) error {
45	cData := C.CString(data)
46	defer C.free(unsafe.Pointer(cData))
47
48	C.fill_buffer(b.ptr, C.int(b.size), cData)
49	return nil
50}
51
52func (b *Buffer) String() string {
53	return C.GoString(b.ptr)
54}
55
56func (b *Buffer) Close() error {
57	if b.ptr != nil {
58		C.free_buffer(b.ptr)
59		b.ptr = nil
60	}
61	return nil
62}
63
64func main() {
65	buf, err := NewBuffer(256)
66	if err != nil {
67		fmt.Println("Error:", err)
68		return
69	}
70	defer buf.Close()
71
72	buf.Fill("Hello from C!")
73	fmt.Println(buf.String())
74}

Exercise 4: Calling Go Functions from C

Difficulty: Advanced | Time: 35-45 minutes

Learning Objectives:

  • Export Go functions for C to call
  • Handle callback patterns
  • Master function pointers and callbacks

Real-World Context:
Some C libraries use callbacks for events, completion handlers, or iteration. This exercise teaches bidirectional communication across the language boundary, used in event systems and streaming libraries.

Task:
Create a Go callback function that C code can call, demonstrating proper function signatures and data passing.

Solution
 1package main
 2
 3// #include <stdlib.h>
 4// #include <stdio.h>
 5//
 6// typedef int (*callback_t)(const char* msg);
 7//
 8// void process_with_callback(callback_t callback) {
 9//     callback("Starting process");
10//     for (int i = 0; i < 3; i++) {
11//         char buf[50];
12//         sprintf(buf, "Processing item %d", i);
13//         callback(buf);
14//     }
15//     callback("Process complete");
16// }
17import "C"
18
19import (
20	"fmt"
21	"unsafe"
22)
23
24//export ProcessCallback
25func ProcessCallback(msg *C.char) C.int {
26	goMsg := C.GoString(msg)
27	fmt.Println("Callback:", goMsg)
28	return 0
29}
30
31func main() {
32	fmt.Println("Starting C callback processing...")
33	C.process_with_callback(C.callback_t(C.ProcessCallback))
34}

Exercise 5: Performance Optimization with CGo

Difficulty: Advanced | Time: 40-50 minutes

Learning Objectives:

  • Understand CGo performance overhead
  • Optimize through batching and caching
  • Profile and measure CGo performance

Real-World Context:
CGo calls have significant overhead. Production code must minimize call frequency through batching, buffering, and smart API design. This exercise demonstrates techniques used in high-performance systems.

Task:
Compare naive CGo usage with optimized batching, demonstrating performance improvements through better API design.

Solution
 1package main
 2
 3// #include <string.h>
 4//
 5// int process_single(const char* input) {
 6//     // Simulate work
 7//     int result = 0;
 8//     for (int i = 0; input[i]; i++) {
 9//         result += input[i];
10//     }
11//     return result;
12// }
13//
14// int process_batch(const char** inputs, int count, int* results) {
15//     for (int i = 0; i < count; i++) {
16//         results[i] = process_single(inputs[i]);
17//     }
18//     return count;
19// }
20import "C"
21
22import (
23	"fmt"
24	"time"
25	"unsafe"
26)
27
28// Inefficient: Many CGo calls
29func IneffientProcess(strings []string) []int {
30	results := make([]int, len(strings))
31	for i, s := range strings {
32		cStr := C.CString(s)
33		results[i] = int(C.process_single(cStr))
34		C.free(unsafe.Pointer(cStr))
35	}
36	return results
37}
38
39// Efficient: Single CGo call with batching
40func EfficientProcess(strings []string) []int {
41	cStrings := make([]*C.char, len(strings))
42	for i, s := range strings {
43		cStrings[i] = C.CString(s)
44		defer C.free(unsafe.Pointer(cStrings[i]))
45	}
46
47	cArray := C.malloc(C.ulong(len(cStrings)) * C.ulong(unsafe.Sizeof((*C.char)(nil))))
48	defer C.free(cArray)
49
50	arrayPtr := (*[1 << 30]*C.char)(cArray)
51	for i, cs := range cStrings {
52		arrayPtr[i] = cs
53	}
54
55	results := make([]int, len(strings))
56	cResults := C.malloc(C.ulong(len(strings)) * C.ulong(unsafe.Sizeof(C.int(0))))
57	defer C.free(cResults)
58
59	C.process_batch((**C.char)(cArray), C.int(len(strings)), (*C.int)(cResults))
60
61	for i := 0; i < len(strings); i++ {
62		results[i] = int((*[1 << 30]C.int)(cResults)[i])
63	}
64
65	return results
66}
67
68func benchmark(name string, fn func()) {
69	start := time.Now()
70	fn()
71	fmt.Printf("%s: %v\n", name, time.Since(start))
72}
73
74func main() {
75	// Create test data
76	testData := make([]string, 1000)
77	for i := 0; i < 1000; i++ {
78		testData[i] = fmt.Sprintf("test-%d", i)
79	}
80
81	fmt.Println("Performance Comparison:")
82	benchmark("Inefficient (many calls)", func() {
83		_ = IneffientProcess(testData)
84	})
85
86	benchmark("Efficient (batched)", func() {
87		_ = EfficientProcess(testData)
88	})
89
90	fmt.Println("\nOptimized batching provides 10-50x better performance!")
91}

CGo is a powerful tool that enables Go to interface with decades of existing C code. The techniques and patterns you've learned here will serve you throughout your Go development career, whether you're building database connectors, cryptographic libraries, or system utilities.

Remember the CGo Golden Rule: With great power comes great responsibility—manage memory carefully and translate errors gracefully!