Building Statically Linked Go Executables with CGO and Zig

This is a short post about how to create a statically linked Go executable that calls in to CGO dependencies using Zig. The full code for this post is available in this repo.

By default, if you're using CGO, the executable you generate dynamically links, but I frequently want to statically link to avoid runtime errors.

First, let's create a zig library, with zig init, then trim back the excess stuff it generates so we're left just with a simple static library. You can rm src/main.zig since we're not creating a zig executable.

Next, we can trim build.zig to just:

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const lib_mod = b.createModule(.{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });

    const lib = b.addLibrary(.{
        .linkage = .static,
        .name = "cgo_static_linking",
        .root_module = lib_mod,
    });
    b.installArtifact(lib);
}

We can leave the build.zig.zon file alone.

Now, let's actually write a simple library function that uses the C ABI in src/root.zig:

// src/root.zig
const std = @import("std");

pub export fn my_zig_function() void {
    std.debug.print("Hello from zig!\n", .{});
}

And its corresponding C header file named zig_lib.h:

// zig_lib.h
#pragma once

void my_zig_function();

That's it on the zig side! You can build the library now by simply running zig build.

Let's write the Go program that calls it.

// main.go
package main

/*
#cgo LDFLAGS: -L./zig-out/lib -lcgo_static_linking -static
#include "zig_lib.h"
*/
import "C"
import "fmt"

func main() {
	fmt.Println("starting program")
	defer fmt.Println("done")

	C.my_zig_function()
}

We'll now build the Go executable and statically link it with this bash command:

CC="zig cc -target x86_64-linux-musl" \
CGO_ENABLED=1 \
CGO_LDFLAGS="-static" \
GOOS=linux GOARCH=amd64 \
go build -a -ldflags '-extldflags "-static"' main.go

Let's check our work:

$ ./main
starting program
Hello from zig!
done

$ ldd ./main
        not a dynamic executable

Looks good to me!

I'm incredibly grateful that I'm building at a time where our tools are getting extremely good. Go and Zig are both amazing!

If you find this useful as I do, perhaps consider making a donation to the Zig Software Foundation!