Skip to content

cgo Basics — Hands-on Tasks

Work through these in order. Requires a C compiler.


Task 1: First cgo program

Write a Go program that calls C's printf to print a greeting.

Acceptance criteria - [ ] Builds with go build. - [ ] Prints "Hello from C!" via C.printf. - [ ] You explain the role of the comment block above import "C".


Task 2: String conversion

Implement a function cToUpper(s string) string that calls C's toupper on each character.

Acceptance criteria - [ ] Returns "HELLO" for "hello". - [ ] Uses C.CString with defer C.free. - [ ] Uses C.GoString (or C.GoStringN) to return.


Task 3: Bench the boundary

Write a benchmark for an empty C function noop(void) {} and an empty Go function func noop() {}.

Acceptance criteria - [ ] Bench both. - [ ] Report the ratio (should be roughly 100×). - [ ] You write a one-sentence implication for hot loops.


Task 4: Pointer-rule violation

Construct code that triggers the "Go pointer to Go pointer" panic. Then fix it.

Acceptance criteria - [ ] You see the panic at runtime. - [ ] You fix by copying to a C-allocated buffer. - [ ] You explain the rule in your own words.


Task 5: KeepAlive necessity

Write a C function that takes a buffer pointer, sleeps 100 ms, then writes to it. Call it from Go with a slice. Use GODEBUG=gctrace=1 to force GC during the call.

Acceptance criteria - [ ] Without KeepAlive, you can sometimes observe the issue (may be intermittent). - [ ] With KeepAlive, the program is stable. - [ ] You document the result.


Task 6: pkg-config

Use // #cgo pkg-config: zlib to link against zlib and compute a CRC32.

Acceptance criteria - [ ] Builds without explicit LDFLAGS. - [ ] Computes the same CRC32 as Go's hash/crc32.


Task 7: Export a Go function to C

Export Multiply(a, b int) int and build as a c-archive.

Acceptance criteria - [ ] go build -buildmode=c-archive produces a .a and a .h. - [ ] A small C program links against them and calls Multiply(3, 4).


Task 8: LockOSThread

Write a goroutine that calls a C function using pthread_self() 10 times and prints the thread ID. Run once without LockOSThread, once with.

Acceptance criteria - [ ] Without lock: thread IDs may differ. - [ ] With lock: thread IDs are identical. - [ ] You explain when this matters in practice.


Task 9: Worker pool for non-thread-safe C

Pretend a C function do_thing(int) is not thread-safe. Build a worker pattern that serializes calls through one goroutine.

Acceptance criteria - [ ] Multiple callers can call concurrently. - [ ] Underlying C function is called serially. - [ ] Caller sees results in the order requested.


Task 10: Static binary

Build a cgo program statically.

Acceptance criteria - [ ] On Linux: go build -ldflags='-linkmode=external -extldflags="-static"' . - [ ] file ./binary reports "statically linked" (or close). - [ ] You document any extra dependencies (e.g., musl-gcc on Alpine).


Task 11: Vendored C source

Add a tiny C library (mylib.c/.h) to your module and call it via cgo.

Acceptance criteria - [ ] The C source is committed in internal/cdep/. - [ ] Cgo flags use ${SRCDIR} to find it. - [ ] Reproducible build: clone, build, no extra setup.


Task 12: Replace cgo with pure Go

Find a pure-Go equivalent for one of: SQLite, libcurl, libpng, openssl. Build a tiny demo of each.

Acceptance criteria - [ ] Pure-Go version compiles without cgo (CGO_ENABLED=0 go build .). - [ ] Functionality matches. - [ ] Bench the two (within reason) and report.


Stretch — Task 13: Cgo and errno

Write a Go wrapper around open(2) and close(2) via cgo. Translate errno to Go errors.

Acceptance criteria - [ ] Opens an existing file, reads data, closes it. - [ ] On error, the Go error contains the human-readable errno string. - [ ] You use the two-return form of cgo to capture errno.


Submission

Code + brief writeup per task. The goal: confident use of cgo with a clear understanding of when (and when not) to reach for it.