DEV Community

Peng Zhang
Peng Zhang

Posted on • Originally published at peng.fyi

You probably want to disable cgo: Go's stdlib has pure-Go implementations

#go

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Build it both ways:

# cgo on (default)
go build -o dns-with-cgo .

# cgo off
CGO_ENABLED=0 go build -o dns-no-cgo .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)