Skip to Content
Course content

211: Calling C Code from Go with cgo

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Look, cgo is one of those features you hope you don't need, but when you're interfacing with a legacy C library or a high-performance system API, it's an absolute lifesaver. It's not as seamless as calling a native Go function, though. You're essentially stepping out of Go's managed world and into the "wild west" of manual memory management. Let's get you comfortable with it.

How do I actually tell Go to use C code?

The magic happens with a "pseudo-package" called C. But here is the catch: you can't just import it. You have to provide a preamble—a block of C code written in a comment immediately preceding the import "C" line. If there's even a single empty line between your comment and the import, the compiler will ignore it and you'll get a bunch of undefined errors.

Suppose we have a legacy C function that calculates a simple checksum of a byte array. Here is how you'd set that up in a Go file:

package main

/*
#include <stdio.h>

int calculate_checksum(const 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 main() {
    // We'll see how to call this in the next section
}

How do I pass a Go string or slice into a C function?

This is where most people trip up. You can't just pass a Go string to a C function because Go strings are managed by the garbage collector (GC), while C expects a pointer to a null-terminated array of bytes. If the GC moves your string around while C is reading it, you've got a memory corruption nightmare on your hands.

To fix this, you use C.CString. This function allocates a new block of memory in the C heap and copies the Go string into it. Because this memory is outside the reach of the Go GC, you must free it manually using C.free. I can't stress this enough: every C.CString needs a corresponding C.free.

package main

/*
#include <stdlib.h>
int calculate_checksum(const 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"
    "unsafe"
)

func GetChecksum(input string) int {
    // Allocate C memory for the string
    cStr := C.CString(input)
    // Ensure we free the memory when the function returns
    defer C.free(unsafe.Pointer(cStr))

    // Call the C function
    // We cast the length of the Go string to a C.int
    result := C.calculate_checksum(cStr, C.int(len(input)))
    
    return int(result)
}

func main() {
    fmt.Println("Checksum:", GetChecksum("Hello cgo!"))
}

Why can't I just use regular int or string types?

You'll notice I used C.int and C.CString. Go is very strict about types, and a C int isn't necessarily the same size as a Go int (which depends on whether you're on a 32-bit or 64-bit architecture). To prevent silent overflows or memory misalignment, cgo creates a separate set of types in the C namespace.

Whenever you're passing data back and forth, you'll be doing a lot of explicit casting. It feels verbose, but it's there to make sure you're consciously acknowledging the boundary between the two languages. If you're dealing with pointers, you'll also find yourself using the unsafe.Pointer type, which acts as a universal bridge that can be cast to any other pointer type.

Is calling C code always faster than writing it in Go?

Actually, no. There is a non-trivial overhead associated with every cgo call. When Go calls C, the runtime has to switch stacks, save registers, and disable certain scheduler optimizations. If you're calling a C function that does something tiny—like adding two numbers—you'll actually find that the Go version is significantly faster because it avoids that "border crossing" penalty.

I usually advise my team to only use cgo when they are calling a massive existing library (like OpenSSL or SQLite) or performing a heavy computational task where the time spent inside the C function vastly outweighs the cost of the call itself.




📋 Practical Task

Exercise: Building a C-Powered String Reverser

In this exercise, you will create a Go program that leverages a C function to reverse a string in-place. This will test your ability to handle C memory allocation and type conversion.

  • Create a Go file that includes a C function void reverse_string(char* str) in the preamble. The C function should take a pointer to a string and reverse its characters.
  • Write a Go wrapper function called Reverse(input string) string.
  • Inside the wrapper, convert the Go string to a C.CString.
  • Call the reverse_string function.
  • Convert the resulting C string back into a Go string using C.GoString.
  • Crucial: Ensure you use C.free to prevent memory leaks.
  • Test your program with the input "Gophers are awesome!" and verify the output is "!emosewa era srehpoG".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.