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 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:
./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.
package main
import (
"fmt"
"net"
)
func main() {
addrs, err := net.LookupHost("amazon.com")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("resolved:", addrs)
}
Build it both ways:
# cgo on (default)
go build -o dns-with-cgo .
# cgo off
CGO_ENABLED=0 go build -o dns-no-cgo .
ldd shows the difference
% file dns-with-cgo
dns-with-cgo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked (uses shared libs), BuildID[sha1]=4d51b80894921ca4be742c64a7dd76c4c7205697, not stripped
% ldd dns-with-cgo
linux-vdso.so.1 (0x00007ffd8efb9000)
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f5bc44b8000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f5bc429a000)
libc.so.6 => /lib64/libc.so.6 (0x00007f5bc3eed000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5bc46ce000)
% file dns-no-cgo
dns-no-cgo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
statically linked, BuildID[sha1]=e8c17c6cb53d034c052336acd4b83a09527bd22c, not stripped
% ldd dns-no-cgo
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
# cgo on: loads libresolv.so.2, then delegates to glibc
% strace -e trace=openat -f ./dns-with-cgo 2>&1 | grep -E "(resolv|\.conf)"
openat(AT_FDCWD, "/lib64/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3 # <-- glibc dependency
[pid 1553] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
[pid 1553] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
resolved: [98.82.161.185 98.87.170.74 98.87.170.71]
# cgo off: Go reads config directly, no .so loaded
strace -e trace=openat -f ./dns-no-cgo 2>&1 | grep -E "(resolv|\.conf)"
[pid 2326] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
[pid 2326] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
resolved: [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.
Top comments (0)