Introduction

Go build constraints (build tags) control which source files are included in a build based on the target operating system (GOOS), architecture (GOARCH), or custom tags. When build constraints are incorrectly specified, the compiler either includes files for the wrong platform (causing compilation errors with platform-specific APIs) or excludes all implementations of a function (causing "undefined" errors). This is common in cross-platform applications that use OS-specific system calls, file paths, or signal handling.

Symptoms

Compilation error due to wrong platform code being included:

bash
# github.com/myapp/internal/sys
sys/sys_unix.go:15:23: undefined: syscall.SIGUSR1
sys/sys_unix.go:18:12: undefined: syscall.ForkExec

Or missing implementation:

bash
# github.com/myapp/internal/sys
sys/process.go:10:9: undefined: getProcessInfo

Or the old syntax warning:

bash
sys_unix.go:1:1: +build line is no longer used in Go 1.17+, use //go:build instead

Common Causes

  • Wrong GOOS in build constraint: Using //go:build windows when the file contains Linux-specific code
  • Missing build constraint on platform-specific files: File without constraint compiles on all platforms
  • Old +build syntax not understood by newer Go: Go 1.17+ requires //go:build syntax
  • File naming convention mismatch: sys_linux.go automatically gets linux constraint, conflicting with manual //go:build
  • Cross-compilation without all platform files: Building for GOOS=windows but no sys_windows.go exists
  • Conflicting constraints: Two files both claim the same GOOS, causing duplicate symbol errors

Step-by-Step Fix

Step 1: Use correct //go:build syntax

```go // sys_linux.go - Linux-specific implementation //go:build linux

package sys

import ( "os" "syscall" )

func GetProcessInfo(pid int) (*ProcessInfo, error) { stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) if err != nil { return nil, err } // Parse /proc/[pid]/stat return parseProcStat(stat) } ```

```go // sys_darwin.go - macOS-specific implementation //go:build darwin

package sys

func GetProcessInfo(pid int) (*ProcessInfo, error) { // Use sysctl on macOS return getProcessInfoViaSysctl(pid) } ```

```go // sys_windows.go - Windows-specific implementation //go:build windows

package sys

func GetProcessInfo(pid int) (*ProcessInfo, error) { // Use Windows API return getProcessInfoViaWindowsAPI(pid) } ```

Step 2: Use file naming convention (preferred)

Go automatically applies build constraints based on file naming:

bash
sys/
├── process.go        # Shared code, no constraints
├── sys_linux.go      # Auto-constraint: //go:build linux
├── sys_darwin.go     # Auto-constraint: //go:build darwin
├── sys_windows.go    # Auto-constraint: //go:build windows
└── sys_other.go      # Auto-constraint: //go:build !linux && !darwin && !windows

No explicit //go:build directive is needed when using the naming convention.

Step 3: Combine OS and architecture constraints

```go // sys_linux_amd64.go //go:build linux && amd64

package sys

func OptimizedFunction() { // Uses AVX instructions available on amd64 } ```

```go // sys_linux_arm64.go //go:build linux && arm64

package sys

func OptimizedFunction() { // Uses NEON instructions available on arm64 } ```

Step 4: Cross-compile with all platforms

```bash # Build for all platforms GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 . GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 . GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64 . GOOS=darwin GOARCH=arm64 go build -o myapp-darwin-arm64 . GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe .

# Verify each build GOOS=linux GOARCH=amd64 go build -v . 2>&1 | grep "sys_linux" ```

Prevention

  • Prefer file naming convention (name_linux.go) over explicit //go:build directives
  • Always provide a fallback implementation in name_other.go with //go:build !linux && !darwin && !windows
  • Cross-compile for all target platforms in CI to catch build constraint errors early
  • Use go tool dist list to see all supported GOOS/GOARCH combinations
  • Add a CI step that builds for every supported platform on every PR
  • Never mix old +build and new //go:build syntax in the same file