Why does a Go file named watch_linux.go disappear from a windows/amd64 build?
answer
- the name is the constraint
- nothing in the file says so
- a suffix after the last underscore
- _linux.go builds only when GOOS=linux
- the missing symbol error names the caller
basics
~20 sThe go tool reads the platform out of the filename: a file ending in _linux.go is compiled only when GOOS=linux. Building for Windows drops it from the package entirely, so every symbol it declared becomes undefined.
solid answer
~50 sFilename suffixes are implicit build constraints. After stripping the extension, a name ending in `_GOOS`, `_GOARCH` or `_GOOS_GOARCH` — in that order, operating system first — restricts the file to that platform, so `watch_linux.go` is invisible to a `GOOS=windows` build. That is how one repository carries a Linux implementation and a Windows implementation of the same helper side by side, with a platform-independent file calling into whichever survived. The failure you see when a platform is missing is a compile error in the shared file, `undefined: watchPath`, or, if nothing at all survives for that target, `build constraints exclude all Go files in <dir>`. Two details catch people out: the rule needs a non-empty prefix, so a file simply called `linux.go` is unconstrained and compiles everywhere, and on tests the platform part goes before `_test`, as in `watch_linux_test.go`.
code
go · 10 lines// watch.go — compiled for every target
func Watch(path string) error {
return watchPath(path)
}
// watch_linux.go — compiled only when GOOS=linux
func watchPath(path string) error { return nil }
// watch_windows.go — compiled only when GOOS=windows
func watchPath(path string) error { return nil }go deeper
Remember that the filename itself decides which platform a Go file is compiled for, and that _linux.go simply is not part of a Windows build. Recognise the undefined-symbol error that follows.
Explain the three suffix forms, the operating-system-first ordering, the non-empty-prefix requirement, and how the shared-caller-plus-per-platform-helper layout keeps exactly one definition alive per target.
Show the release-time consequence: files excluded by name are never compiled, so their errors only appear when someone builds that platform — which is the argument for cross-building every supported target in CI.
Set the convention across a codebase: when a filename suffix is preferable to an explicit constraint, what stub every platform must provide, and how the supported platform list stays in step with the code.
## The rule The go tool decides which files belong to a package before it compiles anything, and part of that decision comes from the filename alone. Strip the `.go` extension and an optional trailing `_test`; if what remains ends with one of - `_GOOS` — for example `watch_linux.go`, `watch_windows.go`, `watch_darwin.go` - `_GOARCH` — for example `hash_amd64.go`, `hash_arm64.go` - `_GOOS_GOARCH` — for example `syscall_linux_amd64.go` then the file is compiled **only** for that platform. Operating system comes first in the combined form. No comment, no flag and no directive is involved; the name is the constraint. This is the mechanism the standard library itself uses heavily, and it is why the same checkout can hold both halves of a platform-specific feature. ## The shape it produces The idiomatic layout is one platform-independent file that declares the exported API and calls an unexported helper, plus one file per platform that defines the helper: ``` watch.go // exported Watch, compiled everywhere watch_linux.go // watchPath for Linux watch_darwin.go // watchPath for macOS watch_windows.go // watchPath for Windows ``` Exactly one definition of `watchPath` survives for any given target, so there is no duplicate-symbol problem. Change `GOOS` and a different one is chosen, with no code in the caller aware of it. ## The two failures If you add a platform to your release matrix without adding its file, the build breaks in one of two ways. **A missing implementation** leaves the shared caller referring to a symbol nobody declared for that target: ``` $ GOOS=windows go build ./internal/watch # example.com/tool/internal/watch internal/watch/watch.go:6:9: undefined: watchPath ``` The error names the *caller*, not the missing file, which is why it reads as a mystery the first time you meet it — the file you expect to see is not absent from disk, it is absent from the build. **No surviving files at all** in a package produces a different message: ``` build constraints exclude all Go files in /path/to/pkg ``` Meaning the directory has Go source, but none of it applies to this target. ## The parsing details that surprise people - **A non-empty prefix is required.** `linux.go` and `amd64.go` are *not* constrained; they compile everywhere. Only the part after the first underscore is examined, which is why the prefix must exist. - **Everything before the first underscore is ignored.** That makes `windows_amd64.go` an amd64-only file with no OS restriction, because the leading `windows` is discarded as the prefix — almost certainly not what the author meant. - **Order matters.** `tool_linux_amd64.go` is Linux on amd64. `tool_amd64_linux.go` is read as a `_GOOS` name with the prefix `tool_amd64`, so it constrains to Linux only and the `amd64` part does nothing. - **Test files put the platform first.** `watch_linux_test.go` is a Linux-only test file; `watch_test_linux.go` is not what you want. - **Leading `_` or `.` excludes a file entirely**, on every platform, which is a different mechanism from the suffix rule and occasionally explains a file that seems to be ignored everywhere. ## Choosing the suffix over an explicit constraint When the split is purely by operating system or architecture, the filename is the cheaper and more readable option: the platform is visible in the directory listing and in the file's tab in an editor, and nobody can edit the first line and silently retarget it. Reach for an explicit build constraint when the condition is anything more complex — a feature toggle, a combination, or a negation. And be aware of the ergonomic cost: a file excluded by its name is not compiled, so a mistake inside it will not be caught until someone builds for that platform. Cross-building each supported target in CI is what turns that latent error into an immediate one.
- Does the toolchain recognise architecture suffixes as well as operating-system ones?Yes. `_amd64.go` and `_arm64.go` constrain by `GOARCH`, and the combined form `_linux_amd64.go` constrains by both — operating system first. Reversing them does not do what it looks like: `tool_amd64_linux.go` is parsed as an OS-only suffix, so it restricts to Linux and the `amd64` part has no effect at all.
- Why is a file simply named linux.go not restricted to Linux?The rule requires a non-empty prefix before the underscore, so only text after the first underscore is examined for a platform name. `linux.go` has no underscore and is compiled everywhere. A related trap is `windows_amd64.go`: the leading part is discarded as the prefix, leaving an amd64-only file with no OS constraint.
- What does the build report when no file in a package survives for the target?`build constraints exclude all Go files in <dir>` — the directory contains Go source, but none of it applies to that `GOOS`/`GOARCH`. In practice it means a platform-specific stub is missing. The usual fix is a file for the new platform that returns a clear "unsupported on this platform" error rather than nothing at all.
- How do these suffixes work on test files?The platform part goes before `_test`, so `watch_linux_test.go` is a Linux-only test file. The tool strips a trailing `_test` first and then applies the platform rule to what is left. Writing `watch_test_linux.go` gives you something that is neither a test file for the linux suffix rule nor what you intended.
saying these in an interview costs you the question
- Thinks the suffix is a naming convention with no effect on the build
- Writes tool_amd64_linux.go and expects both constraints to apply
- Adds a Windows implementation but leaves the shared caller unbuildable
- Believes _linux.go files are excluded only when a flag is passed
- Assumes a file named linux.go is restricted to Linux