One of the advertised features of Go is its ability to build self-contained static executables. This gives us the ability to develop on Arch and deploy on Debian, or even Windows and ARM platforms.

As it turns out, in some cases Go does in fact create dynamically linked executables that expect specific versions of libraries. These are not portable. This happens because Go may use the C compiler even when we do not expect it.

Why does this happen?

$ ldd web
        linux-vdso.so.1 (0x00007fffce3d2000)
        libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f0d8f33d000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f0d8f171000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f0d8f385000)

The developer of GoatCounter pointed out that certain standard Go libraries use C bindings to provide extra functionality.

  • net
  • os/user

When these are used directly or indirectly they have the result of gccgo being used and the executable is dynamically linked by default. However, we can ask them to stay in a pure Go implementation.

Static compilation recipe

go build -tags osusergo
go build -tags netgo
go build -tags osusergo,netgo

One of these build flags will instruct the troubling library to stick to pure Go and give us the static executable we expect.

$ ldd web 
        not a dynamic executable

I think that’s good enough for me but there are options to consider:

CGO_ENABLED=0 go build

This should have the same result. Finally, if we actually do intend to use the C library, we can pass the static compilation flag to the C compiler:

go build -ldflags="-extldflags=-static"

But in that case the executable will not be able to pick up any security updates of the linked libraries. We still have the option of building a dynamically linked executable in a container.