CGo — Integrating C Code with Go
CGo is a mechanism that allows Go programs to directly call C code, and allows C programs to call Go code. It is used for reusing existing C libraries, direct access to system calls, and low-level computations where performance is critical.
CGo Fundamentals
import "C" Virtual Package
// main.go
package main
/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// C functions can be defined directly
int add(int a, int b) {
return a + b;
}
void print_message(const char* msg) {
printf("Output from C: %s\n", msg);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Call C functions
result := C.add(10, 20)
fmt.Printf("C add(10, 20) = %d\n", int(result))
// Go string → C string conversion
msg := C.CString("Hello from CGo!")
defer C.free(unsafe.Pointer(msg)) // Must be released!
C.print_message(msg)
// Use C standard library
cStr := C.CString("hello world")
defer C.free(unsafe.Pointer(cStr))
length := C.strlen(cStr)
fmt.Printf("String length: %d\n", int(length))
}
# CGo build (requires C compiler: gcc or clang)
go build .
go run .
# Disable CGo (pure Go build)
CGO_ENABLED=0 go build .
Core rules:
- Comment containing C code (preamble) must come immediately before
import "C" - No blank line between preamble and
import "C"(otherwise compilation error) - C strings follow the pattern:
C.CString()→ use in Go →C.free()when done
Type Conversion Mapping
package main
/*
#include <stdint.h>
typedef struct {
int x;
int y;
} Point;
Point make_point(int x, int y) {
Point p = {x, y};
return p;
}
double distance(Point a, Point b) {
int dx = a.x - b.x;
int dy = a.y - b.y;
return sqrt((double)(dx*dx + dy*dy));
}
*/
import "C"
import (
"fmt"
"math"
_ "unsafe"
)
func main() {
// Basic type conversion
var goInt int = 42
cInt := C.int(goInt) // Go int → C int
backToGo := int(cInt) // C int → Go int
fmt.Printf("Conversion: %d → %d → %d\n", goInt, cInt, backToGo)
// Use C structs
p1 := C.make_point(0, 0)
p2 := C.make_point(3, 4)
// Access C struct fields
fmt.Printf("p1: (%d, %d)\n", int(p1.x), int(p1.y))
fmt.Printf("p2: (%d, %d)\n", int(p2.x), int(p2.y))
// Call C function
dist := C.distance(p1, p2)
fmt.Printf("Distance: %.2f (expected: %.2f)\n", float64(dist), math.Sqrt(25))
}
Type Mapping Table
| Go Type | C Type | CGo Type |
|---|---|---|
int | int | C.int |
int64 | int64_t | C.int64_t |
float64 | double | C.double |
byte | unsigned char | C.uchar |
string (conversion needed) | char* | C.CString() |
[]byte (conversion needed) | char* | C.CBytes() |
unsafe.Pointer | void* | Direct cast |
External C Library Integration
Linking Shared Libraries
// sqlite_example.go
package main
/*
#cgo LDFLAGS: -lsqlite3
#cgo CFLAGS: -I/usr/include
#include <sqlite3.h>
#include <stdlib.h>
// Wrapper function to handle complex callbacks
static int exec_simple(sqlite3* db, const char* sql) {
char* errmsg = NULL;
int rc = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
if (errmsg) sqlite3_free(errmsg);
return rc;
}
*/
import "C"
import (
"fmt"
"log"
"unsafe"
)
func openDB(path string) *C.sqlite3 {
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 {
log.Fatalf("Failed to open DB: %d", int(rc))
}
return db
}
func execSQL(db *C.sqlite3, sql string) error {
cSQL := C.CString(sql)
defer C.free(unsafe.Pointer(cSQL))
rc := C.exec_simple(db, cSQL)
if rc != C.SQLITE_OK {
return fmt.Errorf("SQL execution failed: %d", int(rc))
}
return nil
}
func main() {
db := openDB(":memory:")
defer C.sqlite3_close(db)
if err := execSQL(db, "CREATE TABLE test (id INTEGER, name TEXT)"); err != nil {
log.Fatal(err)
}
if err := execSQL(db, "INSERT INTO test VALUES (1, 'Go')"); err != nil {
log.Fatal(err)
}
fmt.Println("SQLite operations successful!")
}
Using pkg-config
// openssl_example.go
package main
/*
#cgo pkg-config: libssl libcrypto
#include <openssl/md5.h>
#include <string.h>
void compute_md5(const char* input, unsigned char* output) {
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, input, strlen(input));
MD5_Final(output, &ctx);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func MD5Sum(input string) [16]byte {
cInput := C.CString(input)
defer C.free(unsafe.Pointer(cInput))
var output [16]C.uchar
C.compute_md5(cInput, &output[0])
var result [16]byte
for i, b := range output {
result[i] = byte(b)
}
return result
}
func main() {
hash := MD5Sum("hello world")
fmt.Printf("MD5: %x\n", hash)
// Output: MD5: 5eb63bbbe01eeed093cb22bb8f5acdc3
}
Exporting Go Functions to C
// export_example.go
package main
/*
#include <stdio.h>
// Declare Go functions to be called from C
extern int GoAdd(int a, int b);
extern void GoLog(char* msg);
void c_caller() {
int result = GoAdd(5, 3);
printf("GoAdd(5, 3) = %d\n", result);
GoLog("Successfully called Go function from C!");
}
*/
import "C"
import "fmt"
// Expose to C with //export comment
//
//export GoAdd
func GoAdd(a, b C.int) C.int {
return C.int(int(a) + int(b))
}
//export GoLog
func GoLog(msg *C.char) {
fmt.Printf("Go Log: %s\n", C.GoString(msg))
}
func main() {
C.c_caller()
}
Important:
- No space between
//andexportin the comment - Exported functions use C types for parameters and return values
- Use
C.GoString()to convert C strings to Go strings
Memory Management Patterns
package main
/*
#include <stdlib.h>
#include <string.h>
// C function that allocates memory
char* create_buffer(int size) {
char* buf = (char*)malloc(size);
if (buf) memset(buf, 0, size);
return buf;
}
// C function that frees memory
void destroy_buffer(char* buf) {
free(buf);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
// Go wrapper type for resource management
type CBuffer struct {
ptr *C.char
size int
}
func NewCBuffer(size int) *CBuffer {
ptr := C.create_buffer(C.int(size))
if ptr == nil {
return nil
}
return &CBuffer{ptr: ptr, size: size}
}
func (b *CBuffer) Write(data string) {
cStr := C.CString(data)
defer C.free(unsafe.Pointer(cStr))
C.strncpy(b.ptr, cStr, C.size_t(b.size-1))
}
func (b *CBuffer) Read() string {
return C.GoString(b.ptr)
}
func (b *CBuffer) Free() {
if b.ptr != nil {
C.destroy_buffer(b.ptr)
b.ptr = nil
}
}
func main() {
buf := NewCBuffer(256)
if buf == nil {
fmt.Println("Buffer allocation failed")
return
}
defer buf.Free() // Must be freed
buf.Write("Hello from CGo buffer!")
fmt.Printf("Buffer content: %s\n", buf.Read())
}
Memory Safety Rules
/*
CGo memory rules summary:
1. C.CString() → must call C.free()
2. C.CBytes() → must call C.free()
3. malloc() in C → free() in C (not managed by Go GC)
4. []byte to C → requires pinning
5. Never store Go pointers in C structs (GC can move them)
*/
// Correct slice passing
package main
/*
#include <string.h>
int sum_bytes(char* data, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += (unsigned char)data[i];
}
return sum;
}
*/
import "C"
import "fmt"
func sumBytes(data []byte) int {
if len(data) == 0 {
return 0
}
// Pass pointer to first element of slice (safe)
result := C.sum_bytes((*C.char)(C.CBytes(data)), C.int(len(data)))
return int(result)
}
func main() {
data := []byte{1, 2, 3, 4, 5}
fmt.Printf("Sum of bytes: %d\n", sumBytes(data)) // 15
}
Build Tags and Conditional Compilation
// gpu_accelerated.go
//go:build cgo && linux && amd64
package compute
/*
#cgo LDFLAGS: -L/usr/local/cuda/lib64 -lcuda
#cgo CFLAGS: -I/usr/local/cuda/include
#include "gpu_ops.h"
*/
import "C"
func MatMulGPU(a, b []float32, rows, cols int) []float32 {
// GPU-accelerated matrix multiplication
// ...
return nil
}
// gpu_fallback.go
//go:build !cgo || !linux || !amd64
package compute
// Pure Go fallback for environments without CGo support
func MatMulGPU(a, b []float32, rows, cols int) []float32 {
result := make([]float32, rows*cols)
// CPU implementation...
return result
}
# Makefile — conditional CGo build
.PHONY: build-cgo build-pure
build-cgo:
CGO_ENABLED=1 go build -tags cgo ./...
build-pure:
CGO_ENABLED=0 go build ./...
# Cross-compilation (CGo makes cross-compilation difficult)
# Cross-compilation only works when CGO_ENABLED=0
cross-compile:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/app-linux .
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/app.exe .
Performance and Overhead
CGo function calls have an overhead of ~100ns compared to pure Go calls.
// benchmark_test.go
package main
import (
"testing"
)
/*
#include <math.h>
double c_sqrt(double x) {
return sqrt(x);
}
*/
import "C"
import "math"
func BenchmarkGoSqrt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = math.Sqrt(float64(i))
}
}
func BenchmarkCGoSqrt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = C.c_sqrt(C.double(float64(i)))
}
}
// Example results:
// BenchmarkGoSqrt 1000000000 0.5 ns/op
// BenchmarkCGoSqrt 10000000 120 ns/op ← CGo overhead ~100-200ns
Minimize Overhead with Batch Processing
/*
// Bad: CGo call in loop
for _, val := range largeSlice {
C.process_one(C.double(val)) // ~100ns overhead each time
}
// Good: pass entire slice to C
void process_batch(double* data, int len, double* output) {
for (int i = 0; i < len; i++) {
output[i] = sqrt(data[i]);
}
}
*/
import "C"
import "unsafe"
func processBatch(data []float64) []float64 {
output := make([]float64, len(data))
if len(data) == 0 {
return output
}
// Pass entire slice to C at once → CGo overhead occurs only once
C.process_batch(
(*C.double)(unsafe.Pointer(&data[0])),
C.int(len(data)),
(*C.double)(unsafe.Pointer(&output[0])),
)
return output
}
When NOT to Use CGo
CGo usage decision criteria:
✅ Recommended for CGo
- Reusing mature C libraries (OpenSSL, SQLite, FFmpeg, etc.)
- Direct hardware access (GPU, special device drivers)
- Must integrate with legacy C codebase
- System call wrappers (only possible with CGo)
❌ Avoid CGo in these cases
- Implementation is possible in pure Go
- Cross-compilation is required (CGo needs host C compiler)
- Container/scratch image deployment (C runtime unnecessary)
- Performance-critical hot paths (100ns+ overhead)
- Build must work with Go toolchain only (CI/CD simplification)
Real-World Example — Image Processing Library Wrapper
// image/vips.go
package image
/*
#cgo pkg-config: vips
#include <vips/vips.h>
#include <stdlib.h>
// Track initialization state
static int initialized = 0;
int init_vips(const char* program) {
if (initialized) return 0;
int r = vips_init(program);
if (r == 0) initialized = 1;
return r;
}
// Image resize wrapper
VipsImage* resize_image(const char* filename, int width, int height) {
VipsImage* in;
if (vips_jpegload(filename, &in, NULL)) return NULL;
VipsImage* out;
double scale = (double)width / vips_image_get_width(in);
if (vips_resize(in, &out, scale, NULL)) {
g_object_unref(in);
return NULL;
}
g_object_unref(in);
return out;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func init() {
name := C.CString("go-vips")
defer C.free(unsafe.Pointer(name))
if rc := C.init_vips(name); rc != 0 {
panic("vips initialization failed")
}
}
type Image struct {
vips *C.VipsImage
}
func LoadAndResize(path string, width, height int) (*Image, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
vimg := C.resize_image(cPath, C.int(width), C.int(height))
if vimg == nil {
return nil, fmt.Errorf("image resize failed: %s", path)
}
return &Image{vips: vimg}, nil
}
func (img *Image) Close() {
if img.vips != nil {
C.g_object_unref(C.gpointer(unsafe.Pointer(img.vips)))
img.vips = nil
}
}
Key Takeaways
| Item | Content |
|---|---|
import "C" | Virtual package for C code interface |
| Preamble | /* ... */ comment immediately before import "C" |
C.CString() | Go string → C string (must call C.free()) |
C.GoString() | C string → Go string |
//export | Expose Go function callable from C |
#cgo LDFLAGS | C library linking flags |
#cgo CFLAGS | C compiler flags |
| CGo overhead | ~100-200ns/call (minimize with batch processing) |
- Enable CGo:
CGO_ENABLED=1(default), Disable:CGO_ENABLED=0 - Memory allocated in C must be freed in C (
C.free()) - Never store Go pointers in C structs (GC can relocate them)
- Recommend disabling CGo for cross-compilation