golang / statically linked

  • Written by Walter Doekes

  • Published on: 17/10/2016

So, Go binaries are supposed to be statically linked. That's nice if you run inside cut-down environments where not even libc is available. But sometimes they use shared libraries anyway?

TL;DR: Use CGO_ENABLED=0 or -tags netgo to create a static executable.

Take this example:

$ go version
go version go1.6.2 linux/amd64

$ go build gocollect.go

$ file gocollect gocollect: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, not stripped

$ ldd gocollect linux-vdso.so.1 => (0x00007ffe105d8000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f37e3e10000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f37e3a47000) /lib64/ld-linux-x86-64.so.2 (0x0000560cb6ecb000)

That's not static, is it?

But a minimalistic Go file is:

$ cat >>example.go <<EOF
package main
func main() {

$ go build example.go

$ file example example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
not stripped

Then, why is my gocollect binary not static?

Turns out this is caused by one of the imports. In this case "log/syslog" but others have reported the "net" import to be the cause — which makes perfect sense if the “log/syslog” package imports “net”.

Apparently this was changed between Go 1.3 and 1.4: the “net” stuff brings in a dependency on “cgo” which in turns causes the dynamic linking.

However, that dependency is not strictly needed (*) and can be disabled with Go 1.6 using the CGO_ENABLED=0 environment variable.

(*) The “net” package can use its internal DNS resolver or it can use a cgo-based one that calls C library routines. This can be useful if you use features of nsswitch.conf(5), but often you don't and you just want /etc/hosts lookups and then DNS queries to the resolvers found in /etc/resolv.conf. See the documentation at Name Resolution in golang net.

For the purpose of getting familiar with statically versus dynamically linked binaries in Go, here's a testcase that lists a few options.

cgo.go — must be linked dynamically

package main
// #include <stdlib.h>
// int fortytwo()
// {
//      return abs(-42);
// }
import "C"
import "fmt"
func main() {
    fmt.Printf("Hello %d!\n", C.fortytwo())

fmt.go — defaults to static

package main
import "fmt"
func main() {
    fmt.Printf("Hello %d!\n", 42);

log-syslog.go — pulls in “net” and defaults to dynamic

package main
import "log"
import "log/syslog"
func main() {
    _, _ = syslog.NewLogger(syslog.LOG_DAEMON | syslog.LOG_INFO, 0)
    log.Printf("Hello %d!\n", 42)

Combining the above into a nice little Makefile. (It uses .RECIPEPREFIX available in GNU make 3.82 and later only. If you don't have that, run s/^+ /\t/g.)


BINS = cgo fmt log-syslog ALL_DEFAULT = $(addsuffix .auto,$(BINS)) cgo.auto ALL_CGO0 = $(exclude cgo.cgo0,$(addsuffix .cgo0,$(BINS))) # <– no cgo.cgo0 ALL_CGO1 = $(addsuffix .cgo1,$(BINS)) cgo.cgo1 ALL_DYN = $(addsuffix .dyn,$(BINS)) cgo.dyn ALL_NETGO = $(addsuffix .netgo,$(BINS)) cgo.netgo ALL = $(ALL_DEFAULT) $(ALL_CGO0) $(ALL_CGO1) $(ALL_DYN) $(ALL_NETGO) FMT = %-6s %-16s %s

.PHONY: all clean all: $(ALL)

  • @echo
  • @printf ' $(FMT)\n’ ‘Type’ ‘Static’ ‘Dynamic’
  • @printf ' $(FMT)\n’ ‘—-’ ‘——’ ‘——-’
  • @for x in auto cgo0 cgo1 dyn netgo; do \
  • dyn=`for y in *.$$x; do ldd $$y | grep -q ‘=>’ && \
  • echo $${y%.*}; done`; \
  • sta=`for y in *.$$x; do ldd $$y | grep -q ‘=>’ || \
  • echo $${y%.*}; done`; \
  • printf ' $(FMT)\n’ $$x "echo $${sta:--}" \
  • &quot;`echo $${dyn:--}`&quot;; \
  • done
  • @echo


  • $(RM) $(ALL)

%.auto: %.go

  • go build $< && x=$< && mv $${x%%.go} $@ %.cgo0: %.go
  • CGO_ENABLED=0 go build $< && x=$< && mv $${x%%.go} $@ %.cgo1: %.go
  • CGO_ENABLED=1 go build $< && x=$< && mv $${x%%.go} $@ %.dyn: %.go
  • go build -ldflags -linkmode=external $< && x=$< && mv $${x%%.go} $@ %.netgo: %.go
  • go build -tags netgo $< && x=$< && mv $${x%%.go} $@

You'll notice how I removed cgo.cgo0 from ALL_CGO0 because it will refuse to build.

Running make builds the files with a couple of different options and reports their type:

$ make

Type Static Dynamic

auto fmt cgo log-syslog cgo0 fmt log-syslog - cgo1 fmt cgo log-syslog dyn - cgo fmt log-syslog netgo fmt log-syslog cgo

This table clarifies a couple of things:

  • you get static executables unless there is a need to make them dynamic;
  • you force dynamic executables with -ldflags -linkmode=external;
  • CGO_ENABLED=0 will disable cgo-support, making a static binary more likely;
  • -tags netgo will disable netcgo-support, making a static binary more likely.

I didn't find any Go 1.6 toggle to force the creation of static binaries, but using one of the two options above is good enough for me.

Back to overview