Go CGo — Calling C Code from Go with cgo and Build Tags
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" >}} |