Skip to content

🔗 CGO & FFI

"CGO is not Go." — Rob Pike
CGO cho phép gọi C code, nhưng đổi lại là mất đi nhiều ưu điểm của Go.

⚠️ CGO Trade-offs

🔥 CGO = Complexity

BenefitCost
Access C librariesNo cross-compilation
Reuse existing codeSlower builds
System-level accessNo static binary
Memory safety risks
Debugging difficulty

⚔️ Tradeoff: CGO vs Alternatives

ApproachPerformancePortabilityComplexityWhen to Use
CGONativePoorHighRequired C library
Pure GoGoodExcellentLowRewrite possible
SubprocessOverheadGoodMediumCLI tools
gRPC/FFIRPC overheadGoodMediumService boundary
WASMModerateExcellentMediumSandboxed code

📐 CGO Basics

Simple C Call

go
package main

/*
#include <stdio.h>
#include <stdlib.h>

void greet(const char* name) {
    printf("Hello, %s from C!\n", name);
}

int add(int a, int b) {
    return a + b;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // Call C function
    name := C.CString("Go Developer")
    defer C.free(unsafe.Pointer(name))  // Must free!
    
    C.greet(name)
    
    // Call with return value
    result := C.add(C.int(5), C.int(3))
    fmt.Printf("5 + 3 = %d\n", int(result))
}

Build Requirements

bash
# CGO requires C compiler
# Windows: MinGW-w64 or MSVC
# Linux: gcc
# macOS: Xcode Command Line Tools

# Build with CGO enabled (default)
$ CGO_ENABLED=1 go build

# Check CGO status
$ go env CGO_ENABLED

🔧 Type Conversions

Go C Type Mapping

go
/*
Type mappings:
Go         | C
-----------|------------
C.char     | char
C.schar    | signed char
C.uchar    | unsigned char
C.short    | short
C.int      | int
C.long     | long
C.longlong | long long
C.float    | float
C.double   | double
*/

// String conversion
func cString() {
    // Go string → C string
    goStr := "Hello"
    cStr := C.CString(goStr)      // Allocates C memory!
    defer C.free(unsafe.Pointer(cStr))
    
    // C string → Go string
    goStrBack := C.GoString(cStr)
    
    // With length
    goStrN := C.GoStringN(cStr, C.int(5))
}

// Byte slice conversion
func cBytes() {
    goBytes := []byte{0x01, 0x02, 0x03}
    
    // Go []byte → C void*
    cPtr := unsafe.Pointer(&goBytes[0])
    
    // C void* → Go []byte
    // Use slicing or copy
}

💾 Memory Management

💥 Critical: Memory Ownership

Rule 1: Memory allocated in C must be freed in C. Rule 2: Memory allocated in Go is managed by GC. Rule 3: Passing Go pointers to C has restrictions.

C Memory Allocation

go
/*
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[50];
} User;

User* create_user(int id, const char* name) {
    User* u = (User*)malloc(sizeof(User));
    u->id = id;
    strncpy(u->name, name, 49);
    u->name[49] = '\0';
    return u;
}

void free_user(User* u) {
    free(u);
}
*/
import "C"
import "unsafe"

func main() {
    name := C.CString("Alice")
    defer C.free(unsafe.Pointer(name))
    
    // Create C struct (allocates in C heap)
    user := C.create_user(C.int(1), name)
    defer C.free_user(user)  // Must free!
    
    fmt.Printf("User: id=%d, name=%s\n", 
        int(user.id), 
        C.GoString(&user.name[0]))
}

Go Pointer Rules

go
/*
// cgo pointer passing rules (since Go 1.6):
// 1. Go code may pass a Go pointer to C if it points to memory
//    that doesn't contain any Go pointers.
// 2. C code may not keep a copy of a Go pointer after the call returns.
// 3. C code may not share a Go pointer with other C code.
*/

// ❌ Violates rules - contains Go pointer
type BadStruct struct {
    next *BadStruct  // Go pointer inside
}

// ✅ OK - no Go pointers
type SafeStruct struct {
    id   int32
    data [100]byte
}

func passToC() {
    safe := SafeStruct{id: 42}
    
    // OK - no internal Go pointers
    C.process((*C.SafeStruct)(unsafe.Pointer(&safe)))
}

📚 Linking External Libraries

Using System Library

go
// #cgo LDFLAGS: -lm
// #include <math.h>
import "C"

func main() {
    result := C.sqrt(C.double(16))
    fmt.Println("sqrt(16) =", float64(result))
}

Platform-Specific Flags

go
/*
#cgo CFLAGS: -I/usr/local/include
#cgo LDFLAGS: -L/usr/local/lib

#cgo linux LDFLAGS: -lsqlite3
#cgo darwin LDFLAGS: -lsqlite3
#cgo windows LDFLAGS: -lsqlite3.dll

#include <sqlite3.h>
*/
import "C"

Pkg-config Integration

go
/*
#cgo pkg-config: openssl
#include <openssl/sha.h>
*/
import "C"

💻 Engineering Example: SQLite Wrapper

go
package sqlite

/*
#cgo LDFLAGS: -lsqlite3
#include <sqlite3.h>
#include <stdlib.h>

// Helper: create error string
static char* get_error(sqlite3* db) {
    return (char*)sqlite3_errmsg(db);
}
*/
import "C"
import (
    "errors"
    "unsafe"
)

// DB wraps SQLite database connection
type DB struct {
    db *C.sqlite3
}

// Open opens a SQLite database
func Open(path string) (*DB, error) {
    cPath := C.CString(path)
    defer C.free(unsafe.Pointer(cPath))
    
    var db *C.sqlite3
    
    rc := C.sqlite3_open(cPath, &db)
    if rc != C.SQLITE_OK {
        if db != nil {
            errMsg := C.GoString(C.get_error(db))
            C.sqlite3_close(db)
            return nil, errors.New(errMsg)
        }
        return nil, errors.New("failed to open database")
    }
    
    return &DB{db: db}, nil
}

// Close closes the database connection
func (d *DB) Close() error {
    if d.db == nil {
        return nil
    }
    
    rc := C.sqlite3_close(d.db)
    if rc != C.SQLITE_OK {
        return errors.New(C.GoString(C.get_error(d.db)))
    }
    
    d.db = nil
    return nil
}

// Exec executes a SQL statement
func (d *DB) Exec(sql string) error {
    cSQL := C.CString(sql)
    defer C.free(unsafe.Pointer(cSQL))
    
    var errMsg *C.char
    
    rc := C.sqlite3_exec(d.db, cSQL, nil, nil, &errMsg)
    if rc != C.SQLITE_OK {
        err := C.GoString(errMsg)
        C.sqlite3_free(unsafe.Pointer(errMsg))
        return errors.New(err)
    }
    
    return nil
}

// Usage
func main() {
    db, err := Open(":memory:")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    
    err = db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`)
    if err != nil {
        log.Fatal(err)
    }
    
    err = db.Exec(`INSERT INTO users (name) VALUES ('Alice')`)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println("SQLite operations completed")
}

🔄 Avoiding CGO

Pure Go Alternatives

go
// Instead of CGO SQLite:
import "modernc.org/sqlite"  // Pure Go SQLite

// Instead of CGO image processing:
import "golang.org/x/image/draw"

// Instead of CGO crypto:
import "crypto/sha256"  // Pure Go in stdlib

// Instead of CGO regexp (PCRE):
import "regexp"  // RE2 in pure Go

When CGO is Unavoidable

  • Calling proprietary vendor SDKs
  • Hardware driver interfaces
  • Legacy C library with no Go port
  • Performance-critical code already in C

Ship-to-Prod Checklist

Build & Deploy

  • [ ] CGO_ENABLED=0 if not needed (smaller binaries)
  • [ ] Cross-compilation tested (or use Docker)
  • [ ] Static linking if possible (-ldflags '-extldflags "-static"')
  • [ ] Library versions pinned and documented

Memory Safety

  • [ ] All C allocations freed (C.free, custom free functions)
  • [ ] No Go pointers stored in C beyond call duration
  • [ ] String conversion handled correctly (CString → free)
  • [ ] Memory leaks tested with Valgrind/AddressSanitizer

Testing

  • [ ] CGO code isolated in separate package
  • [ ] Tests run with -race flag
  • [ ] Memory sanitizer used in CI
  • [ ] Multiple platforms tested

📊 Summary

AspectRecommendation
DefaultAvoid CGO if possible
BuildUse Docker for cross-platform CGO
MemoryAlways free C allocations
PointersFollow cgo pointer rules strictly
AlternativePrefer pure Go libraries

➡️ Tiếp theo

CGO nắm vững rồi! Tiếp theo: Performance Tuning - Profiling và optimization.