Skip to main content

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 TypeC TypeCGo Type
intintC.int
int64int64_tC.int64_t
float64doubleC.double
byteunsigned charC.uchar
string (conversion needed)char*C.CString()
[]byte (conversion needed)char*C.CBytes()
unsafe.Pointervoid*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 // and export in 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

ItemContent
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
//exportExpose Go function callable from C
#cgo LDFLAGSC library linking flags
#cgo CFLAGSC 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