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() {
    println("hi")
}
EOF

$ 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.)

.RECIPEPREFIX = +

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:--}`" \
+     "`echo $${dyn:--}`"; \
+ done
+ @echo

clean:
+ $(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.

go golang static dynamic