golang / statically linked
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.