How do filenames like watcher_linux.go and watcher_darwin.go keep per-platform syscall code compiling on every GOOS?
answer
- the go command reads the name
- an underscore then a real target name
- one symbol, one file per platform
- a missing file breaks the build
- your laptop compiles exactly one target
basics
~20 sThe go command applies an implicit build constraint from the filename: x_linux.go compiles only when GOOS=linux. Each platform gets a file defining the same internal function, so one implementation is compiled and callers stay platform-independent.
solid answer
~50 sThe go command reads the filename. A file whose base name ends in `_GOOS`, `_GOARCH` or `_GOOS_GOARCH` — `watcher_linux.go`, `watcher_arm64.go`, `watcher_linux_amd64.go` — carries an implicit build constraint and is compiled only for that target, with no directive inside the file. So you write `watcher_linux.go` and `watcher_darwin.go`, each defining the same unexported `newWatcher` against its own platform calls, and the rest of the package calls `newWatcher` with no build tags anywhere. Two traps: the suffix must follow an underscore after a non-empty base name, so `linux.go` is compiled everywhere and `_linux.go` is ignored by the go tool entirely; and if you claim to support a GOOS with no file for it, the package fails to build there with an undefined-symbol error. That is why CI must cross-compile every supported target — your laptop only ever compiles one.
code
go · 9 lines// watcher_linux.go — compiled only when GOOS=linux
func newWatcher(root string) (*watcher, error) {
return startInotify(root)
}
// watcher_darwin.go — compiled only when GOOS=darwin
func newWatcher(root string) (*watcher, error) {
return startKqueue(root)
}go deeper
Recall that a _linux.go or _darwin.go suffix makes the go command compile that file only for that operating system, with nothing written inside the file itself.
Explain the exact rule — underscore plus a real GOOS or GOARCH value after a non-empty base name — and the pattern of one unexported entry point per platform so callers stay platform-free.
Show you have been burned: a missing platform file is a build error for someone else, an unbuilt file's signature drifts silently, and only a cross-compile matrix in CI catches either.
Own the support matrix as a published promise: which targets you compile, which you actually test, and which get a stub that fails with an unsupported error rather than an undefined symbol.
## The rule Before compiling a package, the go command decides which files belong to it for the current target. Part of that decision comes from the filename alone. If a file's base name, after stripping `.go` and any `_test` suffix, ends in: - `_GOOS` — `watcher_linux.go`, `watcher_darwin.go`, `watcher_windows.go` - `_GOARCH` — `layout_amd64.go`, `layout_arm64.go` - `_GOOS_GOARCH` — `watcher_linux_amd64.go` then the file is compiled only when the target matches. No directive is written inside it. Test files follow the same rule, so `watcher_linux_test.go` runs only on Linux. Two details cause real confusion: 1. **The suffix must follow an underscore, after a non-empty base name.** A file named `linux.go` has no constraint at all and is compiled on every platform — a classic mistake that produces a package that only builds on the author's machine. A file named `_linux.go` is worse: the go command ignores files whose names begin with `_` or `.` entirely, so the code silently disappears from the build everywhere. 2. **Only real GOOS and GOARCH values are recognised.** `watcher_unix.go` is not special — `unix` is not a GOOS — so that file compiles everywhere. Grouping several Unix platforms into one file needs an explicit build constraint line, which is a separate mechanism from this naming convention. ## The pattern it enables The convention exists so platform differences stay at the leaves. Declare one unexported entry point per platform: ```go // watcher_linux.go func newWatcher(root string) (*watcher, error) { return startInotify(root) } // watcher_darwin.go func newWatcher(root string) (*watcher, error) { return startKqueue(root) } ``` Everything else in the package — the indexer loop, the queue, the tests for path handling — calls `newWatcher` and never mentions a platform. The compiler sees exactly one definition per build, so there is no duplicate-symbol problem and no runtime dispatch. Keep the *signature* identical across the files. Since only one is ever compiled, a drifted signature in the file you do not build locally is invisible until someone builds that target. ## The failure mode this creates Omit a platform and the package does not fail gracefully — it fails to compile there, with `undefined: newWatcher`. That is arguably the right outcome, because it is loud, but it is loud *for whoever builds that target*, which may be a user rather than you. So make the support matrix explicit. For platforms you intend to support, write the file and build it in CI. For the rest, provide a fallback file, constrained to "everything else", whose `newWatcher` returns an error saying the platform is unsupported. `errors.ErrUnsupported` is the idiomatic sentinel to wrap, so callers can test the condition rather than the message. ## Why CI has to cross-compile On your machine, `go build ./...` and `go vet ./...` examine one GOOS and one GOARCH. Every other platform file is invisible: not compiled, not type-checked, not vetted. Editors behave the same way. A syntax error, a wrong signature or a constant that does not exist on that platform sits undetected until someone builds it. The cheap defence is a build-only matrix in CI: ``` GOOS=linux GOARCH=amd64 go build ./... GOOS=linux GOARCH=arm64 go build ./... GOOS=darwin GOARCH=arm64 go build ./... ``` Cross-compiling pure Go needs no extra toolchain, so this costs seconds and catches the whole class. Running the *tests* on each platform is a much bigger commitment, and the difference between "we compile for it" and "we test on it" is worth stating in the README rather than leaving to inference. ## Where it fits with the rest of the mechanism The filename convention is a shorthand for the general build-constraint system. Anything the naming cannot express — a group of Unix platforms, a custom tag, a Go-version condition — needs an explicit constraint line at the top of the file. Use the filename where it fits, because it is impossible to get out of sync with the file's contents, and reserve explicit constraints for the cases that genuinely need an expression.
- What happens if a package defines newWatcher only in watcher_linux.go and someone builds it for darwin?The build fails with `undefined: newWatcher`. There is no implicit fallback and no runtime dispatch — the darwin build simply has no definition. It is loud, which is good, but it is loud for whoever builds that target, so a package claiming to support a platform should ship either a real implementation or a fallback file returning an unsupported error.
- Does a file named watcher_unix.go compile only on Unix systems?No. The implicit constraint recognises real GOOS and GOARCH values, and `unix` is neither, so that file compiles on every platform including Windows — usually breaking the Windows build. Grouping several Unix targets into one file requires an explicit build-constraint line, which is a different mechanism from the filename convention.
- How do you catch a compile error in a platform file you never build locally?Cross-compile in CI. `GOOS=darwin GOARCH=arm64 go build ./...` type-checks and compiles that platform's files without needing a Mac, and pure Go cross-compiles in seconds with no extra toolchain. Local builds, `go vet` and editors all see only the current target, so an unbuilt file can carry a wrong signature or a non-existent constant indefinitely.
saying these in an interview costs you the question
- Thinks a file named linux.go is constrained to Linux
- Expects a missing platform file to fall back to a default
- Believes watcher_unix.go compiles only on Unix systems
- Assumes go vet checks files for every platform at once
- Lets the same function's signature drift between platform files