Skip to content

Go CGo — Calling C Code from Go with cgo and Build Tags

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Go CGo. We cover key concepts, practical examples, and best practices to help you master this topic.

Go CGo enables C code integration with import "C" directive, C type mapping, memory management, and build tag conditional compilation.

What You'll Learn

  • Basic CGo usage
  • C type mapping in Go
  • Memory management
  • Build tags for conditional CGo

Why It Matters

CGo bridges Go with C libraries. SQLite driver uses CGo. Image processing uses C libraries. DodaZIP uses CGo for compression libraries.

Real-World Use

Database drivers, image/video processing, system calls, legacy library integration, hardware interfaces.

flowchart LR
    A["CGo"] --> B["import \"C\""]
    A --> C["Type Mapping"]
    A --> D["Memory"]
    A --> E["Build Tags"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Basic CGo

package main

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

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

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

func main() {
    name := C.CString("World")
    defer C.free(unsafe.Pointer(name))
    C.say_hello(name)

    result := C.add(3, 4)
    fmt.Println("3 + 4 =", result)
}

C Type Mapping

/*
#include <stdlib.h>
*/
import "C"
import "unsafe"

func main() {
    // int
    ci := C.int(42)
    gi := int(ci)

    // float
    cf := C.float(3.14)
    gf := float32(cf)

    // string
    str := "hello"
    cstr := C.CString(str)
    defer C.free(unsafe.Pointer(cstr))

    // byte array
    data := []byte("hello")
    cdata := C.CBytes(data)
    defer C.free(cdata)

    // array
    arr := (*C.int)(C.malloc(C.size_t(4 * C.sizeof_int)))
    defer C.free(unsafe.Pointer(arr))
}

Struct and Callback

/*
#include <stdlib.h>

typedef struct {
    int id;
    char* name;
    double score;
} Person;

Person* create_person(int id, const char* name, double score) {
    Person* p = (Person*)malloc(sizeof(Person));
    p->id = id;
    p->name = strdup(name);
    p->score = score;
    return p;
}

void free_person(Person* p) {
    free(p->name);
    free(p);
}
*/
import "C"
import "unsafe"

type Person struct {
    ID    int
    Name  string
    Score float64
}

func createPerson(id int, name string, score float64) Person {
    cName := C.CString(name)
    defer C.free(unsafe.Pointer(cName))

    p := C.create_person(C.int(id), cName, C.double(score))
    defer C.free_person(p)

    return Person{
        ID:    int(p.id),
        Name:  C.GoString(p.name),
        Score: float64(p.score),
    }
}

Build Tags

//go:build cgo

package mylib

// Uses CGo code
//go:build !cgo

package mylib

// Pure Go fallback

Common Mistakes

1. Memory Leaks

cstr := C.CString("hello")
// defer C.free(unsafe.Pointer(cstr))  // Must free!

2. Mixing Go Pointers with C

Don't pass Go pointers to C that contain Go pointers. Use C.malloc for C-owned memory.

3. Cross-Compilation Issues

CGo requires a C compiler for the target platform. Makes cross-compilation harder.

4. CGo Performance Overhead

Each C function call has overhead. Batch C calls when possible.

5. Thread Safety

C code called from multiple goroutines must be thread-safe. Use mutex or limit to single Goroutine.

Practice Questions

1. How do you convert Go string to C string? C.CString creates a C string. Must be freed with C.free. C.GoString converts back to Go string.

2. What does import "C" do? Pseudo-package that bridges to C. Preceding comments contain C code. No import path needed.

3. How do you handle C arrays? Use pointer arithmetic with unsafe.Pointer. Access elements via C helper functions.

4. What are build tags for? Conditional compilation. //go:build cgo includes file only when CGo is enabled.

Challenge: Write a CGo wrapper for a Unix system call.

Solution
/*
#include <unistd.h>
#include <sys/syscall.h>
*/
import "C"

func GetPID() int {
    return int(C.getpid())
}

FAQ

{{< faq question="When should I use CGo?" >}} Only when you must — wrapping an existing C library, or for syscalls unavailable in Go. Prefer pure Go implementations. {{< /faq >}}

{{< faq question="Is CGo portable?" >}} No. Code with CGo requires a C compiler and platform-specific C libraries. Use build tags for platform portability. {{< /faq >}}

{{< faq question="How do I debug CGo?" >}} Use standard C debugging tools (gdb, valgrind). CGo compiles C code with gcc/clang. Set CGO_CFLAGS for debug symbols. {{< /faq >}}

{{< faq question="Can I use C++ with CGo?" >}} Not directly. Write a C wrapper around your C++ code using extern "C". Link the C++ library. {{< /faq >}}

{{< faq question="Does CGo affect garbage collection?" >}} Yes. CGo calls hold OS threads, increasing GC pressure. Minimize CGo calls in hot paths. {{< /faq >}}

Try It Yourself

package main

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

func main() {
    result := C.multiply(6, 7)
    fmt.Printf("6 x 7 = %d\n", int(result))
}

Expected output:

6 x 7 = 42

What's Next

Now that you understand CGo, explore advanced testing techniques in Go.

Topic Description Link
Go Testing Advanced Advanced testing {{< ref "34-testing-advanced" >}}
Go Benchmarking Performance Testing {{< ref "35-benchmarking" >}}
Go Profiling Performance profiling {{< ref "36-profiling" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go