How do filename suffixes like `_linux.go` or `_amd64.go` constrain when a Go file is compiled?
answer
- the name is a constraint too
- only real GOOS and GOARCH values count
- OS comes before architecture
- a leading underscore hides the file completely
- name and header constraint are ANDed
basics
~20 sA Go filename ending in a known GOOS or GOARCH value, such as store_linux.go or store_amd64.go, carries an implicit build constraint for that target. The suffix only counts when the value is a real GOOS or GOARCH and something precedes the underscore.
solid answer
~40 sThe go command reads a file's name as a constraint when it ends in `_GOOS.go`, `_GOARCH.go` or `_GOOS_GOARCH.go` with real values, so `store_windows.go` builds only on Windows and `store_linux_arm64.go` only on Linux on arm64. `_test.go` restricts a file to the test build and combines with the others, as in `store_linux_test.go`. Two traps: the suffix must be a value the toolchain actually knows, so `store_prod.go` has no constraint at all and builds everywhere; and a name must precede the underscore, since a file whose name starts with `_` or `.` is skipped by the go command outright. A filename constraint and a `//go:build` line in the same file are ANDed, so `store_linux.go` containing `//go:build darwin` is compiled by no build at all.
code
text · 8 lineswatcher.go -> every build
watcher_linux.go -> GOOS=linux only
watcher_windows.go -> GOOS=windows only
watcher_linux_arm64.go -> GOOS=linux and GOARCH=arm64
watcher_test.go -> test build only
watcher_linux_test.go -> test build on Linux
watcher_prod.go -> every build (prod is not a GOOS or GOARCH)
_watcher.go -> ignored by the go command entirelygo deeper
Recall that _linux.go, _amd64.go and _test.go restrict a file to a platform or to tests, and that this happens purely from the filename with no comment needed.
Explain the exact rule: only known GOOS and GOARCH values count, OS precedes architecture, _test.go composes with them, and the name's constraint is ANDed with any //go:build line.
Demonstrate that you watch for names that constrain by accident and for asymmetric per-platform file sets, and that you keep an unsuffixed file as the readable entry point into a platform-split package.
Set the convention the team follows — when platform variants live behind filenames versus behind an interface with one implementation per file — so that new engineers can find a definition without knowing the selection rules by heart.
## The rule Besides the `//go:build` comment, the go command derives constraints from the file's own name. After stripping the `.go` extension and an optional `_test` part, a name that ends in `_GOOS`, `_GOARCH` or `_GOOS_GOARCH` is treated as if the file carried the matching constraint: - `watcher_linux.go` — only when the target OS is Linux. - `watcher_arm64.go` — only when the target architecture is arm64. - `watcher_linux_arm64.go` — only when both hold. The order is OS then architecture; the reverse is not recognised. - `watcher_test.go` — only in the test build of the package. - `watcher_linux_test.go` — the test build, on Linux. This is why the standard library is full of paired files with identical function signatures and different bodies: each build picks exactly one. ## The three ways it silently does nothing **The suffix is not a known value.** Only real GOOS and GOARCH names count. `watcher_prod.go`, `watcher_v2.go` and `watcher_internal.go` carry no constraint whatsoever and are compiled in every build. Someone who names a file after an environment expecting it to be selected by `-tags` gets a file that is always on. Custom names are only ever tags in a `//go:build` line; there is no filename form for them. `unix` is a good example: it is a valid term in a `//go:build` expression on recent Go, but it is **not** a valid filename suffix, so `watcher_unix.go` is unconstrained. **There is nothing before the underscore.** The rule needs a name in front. Worse, a file whose name begins with `_` or `.` is ignored by the go command entirely — not merely unconstrained, but invisible to the build, to `go list`, and to package-level tooling. `_watcher.go` is a file nothing will ever compile. **The name was accidental.** Nobody plans to platform-constrain `types_windows.go`; they plan to hold the Windows-specific types there, and that happens to be the same thing. The accident runs the other way too: a helper file someone names `parse_386.go` after a numeric format, or `handler_js.go` after JavaScript payloads, quietly becomes architecture- or OS-specific. `js` and `386` are real targets. ## Combining with `//go:build` A file may have both a constrained name and a `//go:build` line. They are ANDed, not merged or overridden. `watcher_linux.go` with `//go:build darwin` at the top is satisfiable by nothing, and there is no diagnostic: the file simply never enters a build. When you see a `//go:build` line naming a platform on a file whose name already names a different one, that is a bug in every case. The useful corollary is that you should pick one mechanism per file. Either the name carries the platform and the header carries the extra condition — `watcher_linux.go` with `//go:build !cgo` reads clearly — or the name is neutral and the header carries everything. ## `_test.go` is a constraint too It is easy to forget that the test suffix belongs to the same system. A `_test.go` file is excluded from the ordinary package build and included in the test build, which is what lets test-only helpers reference `testing` without dragging it into the shipped binary. It composes with the platform suffixes, so `watcher_windows_test.go` is a test that exists only when testing a Windows build. ## Why any of this matters to a reader The cost of filename constraints is orientation. A newcomer greps for a symbol, finds it in `watcher_darwin.go`, and cannot work out why their Linux build says it is undefined — or, worse, finds nothing at all because the definition they need is in a file their platform excludes. The two habits that pay for themselves are: keep the platform-independent entry points in an unsuffixed file so there is one obvious place to start reading, and keep the per-platform files symmetric, defining exactly the same set of names, so the missing-symbol question has a boring answer.
- A file is named `cache_prod.go` and its author expected `-tags prod` to select it. What actually happens?It is compiled in every build. `prod` is not a GOOS or GOARCH value, so the suffix carries no constraint, and filename constraints never respond to `-tags` anyway. Selecting on a custom tag requires a `//go:build prod` line in the header; the filename mechanism only understands the toolchain's own target values.
- What is built from a file named `store_linux.go` whose header says `//go:build darwin`?Nothing. The filename constraint and the `//go:build` expression are combined with AND, and no build targets Linux and macOS simultaneously, so the file is excluded everywhere. No error is reported — it is exactly the shape of a file that quietly stops being compiled after a rename.
- Why can a file whose name begins with an underscore be worse than one with an unsatisfiable constraint?Because the go command skips it before constraints are even considered, so it does not appear as an ignored file in package listings the way a constraint-excluded file does. It is invisible rather than merely inactive, which makes it harder to notice that the code is not being built.
saying these in an interview costs you the question
- Thinks any suffix after an underscore is a build tag
- Expects -tags to select a file by its name
- Writes the architecture before the OS in the suffix
- Assumes a //go:build line overrides the filename
- Believes _unix.go constrains to unix-like systems