Skip to content
gohacks.dev
Basics

Pick Code by Operating System With Go File Suffixes

A file named name_linux.go or name_arm64.go is built only on that OS or CPU with no build comment. See the valid pairs, the unix suffix trap and how to check.

4 min read
Summarize in AI
Flat illustration of an editor window full of code.

The shortest way to write platform-specific Go is to name the file for the platform. No comment, no tag, no build system: the toolchain reads the end of the file name and decides. It is the technique behind most of os, syscall and net in the standard library. It is also one of those conventions where a single wrong word silently builds the wrong code. This tip covers the pattern, the check that proves it works, and the trap that catches everyone once. It is part of the Go basics path, the file-name companion to build tags, and a common cause of undefined name errors.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The pattern

Put one definition per platform in files named for it. The shared code calls the function, and the toolchain picks:

main.goGo
package main

import "fmt"

func main() { fmt.Println(platform()) }
plat_linux.goGo
package main

func platform() string { return "linux" }
plat_darwin.goGo
package main

func platform() string { return "darwin" }
plat_windows.goGo
package main

func platform() string { return "windows" }
Shell
go run .
GOOS=linux go build -o /dev/null . && echo "linux build ok"
GOOS=windows go build -o /dev/null . && echo "windows build ok"
Text
darwin
linux build ok
windows build ok

On this macOS machine go run . printed darwin, since only plat_darwin.go matched. Setting GOOS cross compiled for the other two, and both succeeded. The names can also combine an OS and an architecture, net_linux_arm64.go, in that order. A file can be limited to an architecture alone, hash_amd64.go, which is how assembly implementations sit beside a pure Go fallback in hash_generic.go with a //go:build !amd64 line.

Prove it covers every target

The compile succeeded for Linux and Windows because both have a definition. A platform nobody wrote one for is the usual failure, and the toolchain says it in the shortest possible way:

Shell
GOOS=freebsd go build -o /dev/null . 2>&1; true
Text
# example.test/lab
./main.go:5:27: undefined: platform

The result is undefined: platform, the same message as any missing name, with nothing in it to suggest a platform file is missing. That is why I run a cross compile for each target in CI, listing the values you support. go tool dist list prints every GOOS/GOARCH pair the toolchain knows, and those are exactly the words that count as suffixes.

The unix trap

//go:build unix is a valid constraint, covering every Unix-like system. The name unix is not a valid file suffix, though, because the suffix list is only operating systems and architectures. A file called plat_unix.go has no restriction at all and is compiled everywhere:

main.goGo
package main

import "fmt"

func main() { fmt.Println(platform()) }
plat_unix.goGo
package main

func platform() string { return "unix" }
Shell
GOOS=windows go list -f '{{.GoFiles}}' .
Text
[main.go plat_unix.go]

The listing for Windows includes plat_unix.go. If a plat_windows.go existed beside it, the two definitions would collide with a platform redeclared error, on Windows only, which is when you discover it. The fix is to say what you mean inside the file, //go:build unix, and name the file anything. This is the most common mistake with the feature, and it is easy to make because unix looks like a platform name. The same applies to any word that is not in go tool dist list: _bsd.go, _posix.go, _mac.go and _win.go are ordinary file names.

Choosing between a suffix and a comment

Use the suffix when a file is wholly for one OS or architecture. It is visible in a directory listing, nobody can misplace it and the toolchain, editors and linters all understand it. Use a //go:build line when the condition is not a single name: linux || darwin, !cgo, amd64 && !purego, or a custom tag. You can combine them. A file named x_linux.go can still carry //go:build !android, and both must hold, which is the answer when you want Linux but not Android, since Go counts android as a linux for suffix purposes.

Where this does not reach

The suffix chooses files, not lines. Differences inside a function, a path separator or a signal name, belong in a small helper per platform, or in runtime.GOOS at run time when the difference is cheap and the code compiles everywhere. Compile-time selection is the right tool when one platform's code does not even build on another, as with system calls. The rules are spelled out in the go command's build constraints section, and the standard library's os package is the best place to see the pattern at scale.

Also inTooling
Taggedquick tip
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.