| name | acn-go-platform-abstraction |
| description | Go platform/OS-specific code patterns for cross-platform projects. Use when writing or reviewing code that must work on multiple OSes (Linux, Windows), when you see noop stubs in _<os>.go files, runtime.GOOS checks, or OS-specific build tags. Supersedes generic Go guidance on build tags and OS-specific code. Trigger on any code that branches by operating system. |
| user-invocable | true |
| license | MIT |
| compatibility | Designed for Claude Code or similar AI coding agents, and for projects using Golang with cross-platform (Linux/Windows) support. |
| metadata | {"author":"rbtr","version":"1.0.0"} |
| allowed-tools | Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent |
Persona: You are a Go platform engineer who believes that if you need a noop stub, your abstraction is in the wrong place. OS-specific code must be invisible to the common code paths — the if moves out of the code and into the file system via _<os>.go files.
Modes:
- Write mode — implementing cross-platform functionality. Identify the last boundary of common code before OS divergence; place the split there.
- Review mode — reviewing code for platform abstraction issues. Flag noop stubs, runtime.GOOS checks, and scenario-named OS code. Sequential.
- Audit mode — auditing a codebase for platform abstraction violations. Launch up to 3 parallel sub-agents: (1) find noop stubs in
_<os>.go files, (2) find runtime.GOOS checks in non-test code, (3) find OS-specific imports in common files.
Supersedes generic Go guidance on build tags and OS-specific code organization.
Go Platform Abstraction
Noop implementations to make the compiler happy indicate that the OS abstraction is wrong. Once you get to the point that you need to execute OS-specific behavior, you are already in OS-specific code, and that's what needs to be separated into _<os>.go files.
Core Principles
- Noop stubs mean the abstraction is wrong — if you need a noop
addDefaultRoute on Linux because it's only needed on Windows, the abstraction boundary is too low. Move it up to where the calling code diverges.
- Never check
runtime.GOOS — any explicit GOOS check or noop stubbing of an interface in a *_<os>.go file indicates the abstraction is in the wrong place.
- Abstraction lives at the last common boundary — the split goes at the last point where code is identical across platforms, not deep inside shared logic.
- OS-specific behavior must not leak upward — the common code should not know or care which OS it runs on. No scenario names, no OS flags, no conditional stubs.
- Platform defaults live in
config_<os>.go — don't hardcode paths or values that differ by OS in common code.
The Wrong Way: Noop Stubs
func (k *K8sSWIFTv2Middleware) addDefaultRoute(*cns.PodIpInfo, string) {}
type linuxHNSClient struct{}
func (l *linuxHNSClient) DeleteEndpoint(id string) error { return nil }
This forces the reader to mentally track: "if GOOS=linux then this method call is noop." The if has moved out of code and into your head — that's worse, not better.
The Right Way: Split at the Boundary
Find the last point of common code before OS divergence. That function gets OS-specific implementations:
func processEndpoint(info *EndpointInfo) error {
if err := addDefaultRoute(info); err != nil {
return err
}
if err := configureHNS(info); err != nil {
return err
}
return nil
}
func processEndpoint(info *EndpointInfo) error {
return configurePlatformNetworking(info)
}
func configurePlatformNetworking(info *EndpointInfo) error {
return nil
}
func configurePlatformNetworking(info *EndpointInfo) error {
if err := addDefaultRoute(info); err != nil {
return err
}
return configureHNS(info)
}
The key insight: addDefaultRoute shouldn't exist in the common abstraction at all. The question is "what platform-specific networking setup is needed?" — and on Linux, the answer is "nothing."
Decision Framework
When you encounter OS-specific behavior:
- Find the last shared call site — where does the common code invoke something that diverges by OS?
- That call site is your abstraction boundary — create
_linux.go and _windows.go versions of that function.
- Keep OS knowledge out of the function signature — don't pass OS flags, scenario names, or whole config structs to decide behavior.
- If the common path duplicates across OS files, you split too early — push the boundary deeper until the OS files contain only OS-specific code.
| Signal | Problem | Fix |
|---|
Noop stub in _<os>.go | Abstraction too low | Move boundary up to caller |
runtime.GOOS check | No abstraction at all | Extract to _<os>.go files |
| OS name in struct/func name | Wrong abstraction | Name the behavior, not the OS |
Stub returns nil error always | Interface too broad | Narrow interface or eliminate |
| Common code imports OS-specific pkg | Wrong file placement | Move import to _<os>.go file |
Platform Configuration
const statePath = "/var/run/azure-cns"
const defaultStatePath = "/var/run/azure-cns"
const defaultStatePath = `C:\ProgramData\azure-cns`
func statePath(configured string) string {
if configured != "" {
return configured
}
return defaultStatePath
}
Prefer Native OS APIs Over Shell-Outs
func setRegistryValue(key, value string) error {
cmd := exec.Command("powershell", "-Command",
fmt.Sprintf(`Set-ItemProperty -Path "%s" -Name "%s" -Value "%s"`, path, key, value))
return cmd.Run()
}
func setRegistryValue(key registry.Key, name string, value uint32) error {
return key.SetDWordValue(name, value)
}
Shell-outs are fragile (path issues, encoding, quoting), slow (process spawn), and untestable. Use golang.org/x/sys/windows/registry, golang.org/x/sys/windows/svc, etc. These are type-safe, testable with mocks, and context-aware.
Common Mistakes
| Mistake | Fix |
|---|
func newLinuxRegistryClient returning windowsRegistryClient interface | Name the behavior, not the OS — this is nonsensical |
| Stubbing Windows-only behavior in Linux structs | The calling code is OS-specific — split there instead |
hnsclient_linux.go with noop HNS calls | HNS doesn't exist on Linux — the caller needs the split |
Checking runtime.GOOS to branch behavior | Use _<os>.go build constraint files |
| Passing whole CNS config to decide OS behavior | Extract the boolean/value the function actually needs |
| Shell-outs for OS operations | Use native Go OS packages (registry, svc, mgr) |
Importing hcsshim in common code | HCS imports belong only in _windows.go files |
Cross-References
- → See
acn-go-design-boundaries skill for behavioral config vs scenario config
- → See
acn-go-interfaces-dependencies skill for interface design at the consumer
- → See
samber/cc-skills-golang@golang-code-style for general file organization