33.4 CGo: Calling C Code from Go
Alright, let’s talk about CGo. You’ve probably heard the horror stories. It’s the part of Go that feels like it was designed by a committee who had a very, very tense meeting with the C standards body and then decided to duct-tape the two languages together. And you know what? It kind of was. But sometimes, you just have to talk to a C library. Maybe it’s a rock-solid database client, a hardware SDK, or that one numerical library that’s been optimized within an inch of its life. When you need to, CGo is your bridge. It’s a powerful tool, but it’s also a footgun of spectacular proportions if you don’t respect it.
The first thing to understand is that importing C is magic. No, really. You don’t import a package; you import the concept of C itself with a special pseudo-package. The comment immediately above the import "C" statement is not a comment in the traditional sense—it’s CGo preamble code. The compiler literally parses it and uses it as header information. Forget this, and you’ll have a very bad time.
/*
#include <stdio.h>
#include <stdlib.h>
// Your C functions, typedefs, and #defines go here.
// This isn't just for show; this code is plopped into the C side of the generated code.
*/
import "C"
import (
"unsafe" // You'll become very familiar with this package.
)
The Basic Mechanics: Your First C Function Call
Let’s start with the “Hello, World” of CGo: calling a simple C function. Here’s how you wrap a call to C’s puts (or printf—but let’s not get ahead of ourselves).
package main
/*
#include <stdio.h>
*/
import "C"
func main() {
// We're calling C.puts, not Go's fmt.Println.
// The C string is converted from a Go string automatically.
C.puts(C.CString("Hello from the C world!"))
}
Run that. It works! But I just committed a cardinal sin. Did you spot it? We allocated a C string using C.CString and then promptly leaked it like a sieve. C.CString uses malloc to allocate memory on the C heap. The Go garbage collector, brilliant as it is, doesn’t know about that memory. It will never free it. This is Pitfall #1, and it’s the easiest way to introduce memory leaks into your application.
Doing It Right: Memory Management is Your Job
The correct way is to immediately defer the freeing of that memory. You must manage C-allocated memory explicitly. There’s no way around it.
package main
/*
#include <stdio.h>
#include <stdlib.h> // This is needed for free()
*/
import "C"
import "unsafe"
func main() {
cs := C.CString("This one won't leak!")
defer C.free(unsafe.Pointer(cs)) // Defer the call to C's free()
C.puts(cs)
}
Notice the unsafe.Pointer. That’s your signal that you’re leaving the safety of Go’s world. You’re telling the compiler, “I, the programmer, take full responsibility for this.” It’s the equivalent of a giant “HERE BE DRAGONS” sign on a map. Use it with extreme prejudice.
Passing Data Back and Forth: It’s All C Types Now
When you cross the boundary, you’re dealing with C types, not Go types. C.int, C.double, C.char, etc. You must convert between Go and C types explicitly. This is often verbose and always tedious, but it’s crucial for clarity.
Let’s say you have a C function you want to call:
// In your preamble:
int add(int a, int b) {
return a + b;
}
Your Go code to call it would look like this:
package main
/*
int add(int a, int b) {
return a + b;
}
*/
import "C"
import "fmt"
func main() {
a := 10
b := 20
// Convert Go ints to C ints, call the function, convert the result back.
result := int(C.add(C.int(a), C.int(b)))
fmt.Printf("%d + %d = %d\n", a, b, result)
}
The Performance Elephant in the Room
Here’s the part the manuals often gloss over: CGo calls are expensive. Monumentally so, by Go’s standards. A call from Go into C isn’t a simple function call. It’s a full-blown context switch between goroutine stacks and C system stacks. The runtime has to lock threads and do a bunch of accounting. We’re talking hundreds of nanoseconds of overhead per call.
This means you should never, ever use CGo for fine-grained calls inside a tight loop. The design pattern is to create a coarse-grained C API where you pass a buffer of data to C, let C do all the heavy lifting on that buffer, and then return a result. One CGo call doing a lot of work is fine; a million CGo calls doing a tiny amount of work each will destroy your performance.
The Build Process: It Gets Weird
When you use go build on a package with CGo, it doesn’t just invoke the Go compiler. It creates a temporary directory, writes out all the C code from your preamble, compiles it with a C compiler (like gcc or clang), and then links it with the Go code. This means:
- You need a C toolchain installed. This is a massive portability hurdle.
- You inherit all of C’s build complexities.
#cgodirectives in your preamble let you specify compiler and linker flags. Need to link a specific library? You’ll do it here.
/*
#cgo LDFLAGS: -lm -lmy_special_library
#include <math.h>
#include <my_special_library.h>
*/
import "C"
This is both a superpower and a maintenance burden. You’re now writing build instructions inside comments in your Go code. It’s as bizarre as it sounds.
So, should you use CGo? The answer is: only if you absolutely must. It’s a fantastic escape hatch for when you have no other choice, but it comes at a real cost in performance, complexity, and portability. Use it to wrap existing C libraries into coarse-grained APIs, not to write new logic. And for the love of all that is holy, always remember to free.