You can probably disable cgo: Go's stdlib has pure-Go implementations
CGO_ENABLED defaults to 1. That means a standard go build produces a binary
that links against C libraries (e.g., glibc) at runtime. For many parts of the Go standard library,
there is a C-backed implementation and a pure-Go implementation. CGO_ENABLED selects
which one gets compiled in. Pure-Go alternatives also exist for many third-party libraries,
so it is likely you can turn off cgo by setting CGO_ENABLED=0.
Whether to use C libraries or not is a build-time decision, not a runtime fallback like
"foo.so doesn't exist, fall back to pure-Go". If the required .so is missing when the
cgo binary runs, the dynamic linker fails immediately with an error like:
1./dns-with-cgo: error while loading shared libraries: libresolv.so.2: cannot open shared object file: No such file or directory
Example: DNS resolution with net.LookupHost
With cgo enabled, net.LookupHost delegates to glibc's getaddrinfo via libresolv.so.2.
With cgo disabled, the same call uses Go's built-in resolver that reads /etc/resolv.conf
directly, with no C library involved. Same API either way.
1package main
2
3import (
4 "fmt"
5 "net"
6)
7
8func main() {
9 addrs, err := net.LookupHost("amazon.com")
10 if err != nil {
11 fmt.Println("error:", err)
12 return
13 }
14 fmt.Println("resolved:", addrs)
15}
Build it both ways:
1# cgo on (default)
2go build -o dns-with-cgo .
3
4# cgo off
5CGO_ENABLED=0 go build -o dns-no-cgo .
ldd shows the difference
1% file dns-with-cgo
2dns-with-cgo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
3dynamically linked (uses shared libs), BuildID[sha1]=4d51b80894921ca4be742c64a7dd76c4c7205697, not stripped
4
5% ldd dns-with-cgo
6 linux-vdso.so.1 (0x00007ffd8efb9000)
7 libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f5bc44b8000)
8 libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f5bc429a000)
9 libc.so.6 => /lib64/libc.so.6 (0x00007f5bc3eed000)
10 /lib64/ld-linux-x86-64.so.2 (0x00007f5bc46ce000)
11
12% file dns-no-cgo
13dns-no-cgo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
14statically linked, BuildID[sha1]=e8c17c6cb53d034c052336acd4b83a09527bd22c, not stripped
15
16% ldd dns-no-cgo
17 not a dynamic executable
The cgo binary dynamically links against libresolv.so.2, libc.so.6, and other glibc libraries. The
CGO_ENABLED=0 binary is fully static — no shared libraries at all.
strace confirms it at runtime
1# cgo on: loads libresolv.so.2, then delegates to glibc
2% strace -e trace=openat -f ./dns-with-cgo 2>&1 | grep -E "(resolv|\.conf)"
3openat(AT_FDCWD, "/lib64/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3 # <-- glibc dependency
4[pid 1553] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
5[pid 1553] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
6resolved: [98.82.161.185 98.87.170.74 98.87.170.71]
7
8# cgo off: Go reads config directly, no .so loaded
9strace -e trace=openat -f ./dns-no-cgo 2>&1 | grep -E "(resolv|\.conf)"
10[pid 2326] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
11[pid 2326] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
12resolved: [98.82.161.185 98.87.170.74 98.87.170.71]
With cgo, the first thing that happens is loading the C library. Without cgo, Go skips straight to reading the resolver config itself.
Why CGO_ENABLED=0 matters
A cgo binary built on one Linux may depend on libc.so.6 being present and ABI-compatible
on the target. That's usually fine, but it's an implicit runtime dependency you may
not notice until deployment fails on a minimal OS, or an OS with incompatible libc versions, or inside a scratch container image with no libc. CGO_ENABLED=0 removes that dependency entirely. The binary becomes fully static
and runs on any Linux system regardless of what libc is installed.
Conclusion
Go's pure-Go implementations are not always full replacements. The pure-Go DNS
resolver doesn't support all NSS plugins, so environments relying on custom NSS
modules for service discovery or LDAP may see different behavior. For standard DNS
over /etc/resolv.conf, it works fine.
Wherever the stdlib has both a cgo and a pure-Go path,
CGO_ENABLED=0 at build time opts into the pure-Go one. You get a simpler, more
portable binary at the cost of potentially narrower feature coverage in those specific
areas. Just remember the choice is made when you run go build, not when the binary
runs on the target machine.