skip to content

What do the netgo and osusergo build tags do when cgo stays enabled?

level: middleimportance: should knowfreq 42%

answer

  1. flags you pass with -tags
  2. when cgo cannot be turned off
  3. two stdlib packages, one Go implementation each
  4. changes behaviour, not the linking
  5. ldd still lists shared libraries

basics

~20 s

Both are build tags that force a pure-Go implementation even with cgo enabled: netgo pins the net package to its Go hostname resolver, osusergo pins os/user to the version parsing /etc/passwd. Neither makes the binary static.

solid answer

~40 s

`netgo` and `osusergo` are build tags recognised by the standard library, used as `go build -tags netgo,osusergo`. They exist for the case where you cannot set `CGO_ENABLED=0` — some dependency genuinely needs cgo — but you still want the two packages that behave differently under cgo to use their Go implementations. `netgo` selects the pure-Go hostname resolver instead of the path that calls the C library; `osusergo` selects the `os/user` implementation that reads `/etc/passwd` and `/etc/group` directly. The important limitation is that they change *behaviour*, not *linking*: if any package in the build still uses cgo, the binary is still dynamically linked against the host C library and still will not start on a host with a different one. They buy you predictable lookup behaviour, not a portable artifact.

code

text · 4 lines
text
$ CGO_ENABLED=1 go build -tags netgo,osusergo -o app .
$ ldd ./app
        linux-vdso.so.1 (0x00007ffc...)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)

go deeper

for a junior

Know that these are build tags passed with go build -tags, and that each one picks a Go implementation of a standard-library package instead of one that calls the host C library.

for a middle

Explain why they exist at all: cgo cannot always be turned off, and these pin the two packages whose behaviour depends on it. Be clear that they do not affect linking.

for a senior

Use them as a diagnostic. Rebuilding the same code with the tag and re-running is a fast A/B that tells you whether a resolution difference between two environments comes from the C library path.

for a principal

Decide whether the tags belong in the standard build line or only in an exception. Pinning behaviour at build time is a policy choice with the same cost as disabling cgo, and it should be recorded, not copied between repositories.

## The problem these tags solve There are two separate things people want from `CGO_ENABLED=0`. One is a *self-contained artifact*: a single file with no libc dependency. The other is *predictable lookup behaviour*: hostname and user resolution that does the same thing on a laptop, in CI and in production, because it is Go code compiled into the binary rather than whatever the host's C library decides to do. Sometimes you cannot have the first. A dependency binds a C library, or the platform requires it, and cgo must stay on. `netgo` and `osusergo` let you take the second half anyway. ## What each tag selects The standard library ships two implementations of a small number of things, guarded by build constraints: - **`netgo`** — the `net` package has a hostname resolver written entirely in Go and a path that calls into the host C library. Without the tag and with cgo available, the decision between them is made at run time from the host's configuration. Building with `-tags netgo` fixes the choice at build time: the Go resolver, always. - **`osusergo`** — `os/user` has a pure-Go implementation that parses `/etc/passwd` and `/etc/group`, and a cgo one that calls the C library's password-database functions. `-tags osusergo` selects the Go one. Usage is ordinary `go build` tag syntax: ``` go build -tags netgo,osusergo -o app . ``` There are counterpart tags in the other direction (`netcgo` forces the C path), which is worth knowing mainly so you recognise one in someone else's build script. ## The limitation that trips people up A build tag does not turn cgo off. If *anything* in the dependency graph still uses cgo, the Go linker still hands off to the system C toolchain and you still get a dynamically linked executable that names an ELF interpreter and lists shared libraries. `file` and `ldd` will say so. That binary is still coupled to the C library it was linked against, so it still fails to start on a host with a different one. In other words: `netgo`/`osusergo` fix *which code runs*. They do not fix *what the binary needs at load time*. Teams that add `-tags netgo` hoping for a portable artifact and then find the same startup failure in production have conflated the two. The converse is also worth stating: if nothing in the build uses cgo, you did not need the tags — `CGO_ENABLED=0` already selects the same implementations and gives you the static link as well. The tags are the tool for the mixed case, not the default. ## When you actually reach for them - A service must link a C library for one feature, but its hostname lookups must behave identically everywhere. `-tags netgo` makes the resolver a property of the build rather than of the host. - You want to *remove a variable* while debugging a resolution difference between two environments: rebuild with `-tags netgo`, run the same code again, and see whether the difference survives. That A/B is often faster than reading configuration files on both hosts. - A container-style environment has no complete `/etc/passwd` and `os/user` behaves oddly; `osusergo` at least makes the behaviour the same in every environment, so you can reason about it. ## What to say about the cost Selecting the Go implementations has the same downside as disabling cgo altogether: the Go resolver understands the ordinary configuration sources but cannot load the C library's pluggable name-service modules, so names that only exist behind one of those stop resolving. The tag is not free — it is the same behavioural trade, taken deliberately, in a build that still needs cgo for other reasons. ## Checklist for an interview answer 1. They are build tags, passed with `-tags`. 2. Each pins one standard-library package to its pure-Go implementation. 3. They matter only when cgo is enabled — with `CGO_ENABLED=0` you already have those implementations. 4. They do not make the binary static, and `ldd` is how you prove that to a colleague who thinks otherwise.

  • If you already build with CGO_ENABLED=0, is there any point in adding -tags netgo,osusergo?
    No. With cgo disabled the cgo implementations are not compiled in at all, so the Go ones are the only choice and the tags are redundant. They are harmless, but seeing them alongside `CGO_ENABLED=0` usually means someone was cargo-culting a build line rather than reasoning about it.
  • A colleague adds -tags netgo and is surprised the binary still will not run on another host. What do you tell them?
    That the tag chose which resolver code runs, not how the binary is linked. Some other package in the build still uses cgo, so the Go linker used the external linker and the executable still depends on the host C library. Show them `ldd` on the artifact; the fix is removing the cgo dependency, not another tag.

saying these in an interview costs you the question

  • Thinks netgo makes the binary statically linked
  • Believes the tags are runtime environment variables
  • Adds both tags alongside CGO_ENABLED=0 and calls it necessary
  • Assumes the tags restore lookups the pure-Go code cannot do