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:
- Preamble: The
/* ... */comment contains C code that CGo compiles separately - Import:
import "C"creates the bridge pseudo-package - Type conversion:
C.int,C.char*map between Go and C types - 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:
- Memory management is critical - Every
C.malloc()needs a correspondingC.free() - Type conversion is bidirectional - Go ↔ C data types need careful handling
- Error handling translation - Map C return codes to Go errors appropriately
- Thread safety - C libraries often need Go-level synchronization
- 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
//exportto make Go functions callable from C - Error propagation - Translate C error codes to Go error interfaces
CGo Workflow
- Design the interface - Plan clean Go API around C library
- Implement C wrappers - Create safe Go functions around C functions
- Handle memory management - Use defer, finalizers, and clear ownership rules
- Add thread safety - Protect C library calls with Go synchronization
- 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:
- Transitions from Go to C runtime
- Marshals arguments (copies data)
- Executes the C function
- Marshals return values
- 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:
- Performance: Access hand-optimized C libraries
- Ecosystem: Integrate with decades of existing code
- Compatibility: Support platforms with specific C requirements
- 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!