syscall & Pure-Go Alternatives to CGO
Many tasks that once required cgo now have pure-Go paths: golang.org/x/sys for syscalls, community ports of crypto and compression libraries, and stdlib packages that wrap OS APIs directly.
Choosing pure Go first keeps cross-compilation, static binaries, and CI simpler.
Summary
Prefer maintained pure-Go libraries when they cover your platform matrix and performance needs.
Reach for cgo when a vendor SDK, hardware driver, or OS API exists only as C headers without a safe Go wrapper.
The syscall package is deprecated for new code; use x/sys/unix, x/sys/windows, or stdlib os/net instead.
Recipe
Quick-reference recipe card - copy-paste ready.
import (
"golang.org/x/sys/unix"
)
func openReadOnly(path string) (int, error) {
fd, err := unix.Open(path, unix.O_RDONLY, 0)
if err != nil {
return -1, err
}
return fd, nil
}When to reach for this:
- You need POSIX or Windows syscalls without linking custom C.
- CI must build with
CGO_ENABLED=0. - You want single-command cross-compiles (
GOOS,GOARCH). - An
x/sysor stdlib wrapper already exists for the API.
Working Example
List directory entries with x/sys on Unix instead of cgo-wrapped libc.
package main
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
func main() {
dir, err := os.Open(".")
if err != nil {
panic(err)
}
defer dir.Close()
// Prefer os.ReadDir in application code; unix.ReadDirent shows x/sys shape
buf := make([]byte, 4096)
for {
n, err := unix.ReadDirent(int(dir.Fd()), buf)
if err != nil || n == 0 {
break
}
// Parse buf per unix.ParseDirent in production helpers
fmt.Printf("read %d bytes of dirent data\n", n)
}
}What this demonstrates:
- File descriptors from
os.Filefeedunixsyscalls without cgo. golang.org/x/systracks platform differences per GOOS.- Application code should usually prefer higher-level
osAPIs;x/sysis for gaps.
Deep Dive
How It Works
- Pure-Go syscall packages use assembly stubs or compiler intrinsics generated per platform to invoke the kernel trap interface.
- No C compiler is required;
go buildwithCGO_ENABLED=0still succeeds. golang.org/x/sysis the maintained home for low-level constants and functions; the stdlibsyscallpackage is frozen for compatibility.- Community ports (SQLite pure Go, imaging, crypto) reimplement algorithms in Go or use
//go:embedfor data tables instead of linking C.
Decision Signals
| Signal | Lean pure Go | Lean cgo |
|---|---|---|
Platform APIs in x/sys | Yes | No |
Vendor .h + .so SDK | No | Yes |
CGO_ENABLED=0 required | Yes | No |
| Decades-old C library with tests | Maybe port | Often wrap |
| Hardware DMA / GPU driver | Rare in pure Go | Usually cgo |
Common Pure-Go Sources
| Need | First look |
|---|---|
| Files, env, processes | os, os/exec |
| Sockets | net, syscall constants via x/sys |
| Terminal / ioctl | golang.org/x/term, x/sys/unix |
| SQLite | modernc.org/sqlite (pure) vs mattn/go-sqlite3 (cgo) |
| Compression | compress/*, klauspost/compress |
| Crypto | crypto/* stdlib |
Go Notes
//go:build !cgo
package db
import _ "modernc.org/sqlite" // database/sql driver, no cgoMirror with //go:build cgo files when you must support both driver backends behind one sql.Open DSN scheme.
Gotchas
- Using deprecated
syscallpackage for new code - Misses fixes inx/sys. Fix: Importgolang.org/x/sys/unixorwindowsand migrate call sites. - Assuming pure-Go means slower - Modern ports often match cgo for I/O bound work; CPU-bound crypto may differ. Fix: Benchmark on your workload before choosing.
- Picking
mattn/go-sqlite3in static CI - Requires cgo and musl/glibc care. Fix: Usemodernc.org/sqlitewhenCGO_ENABLED=0is non-negotiable. - Reimplementing ioctl by guessing struct layouts - Breaks across kernel versions. Fix: Follow
x/sysdefinitions or official OS docs; add build tags per arch. - Ignoring build tags on optional cgo features - Consumers cannot compile without a C toolchain. Fix: Ship
!cgoimplementations or clear documentation of hard cgo requirement. - RPC to avoid cgo when latency budget is microseconds - Network cost dwarfs saved build pain. Fix: Measure end-to-end before rejecting in-process cgo.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
golang.org/x/sys | Documented syscalls per GOOS | API exists only in vendor C headers |
Stdlib os/net | Everyday file and network I/O | You need raw epoll/kqueue control |
| Pure-Go third-party port | Feature parity and license OK | Port is unmaintained or incomplete |
| cgo wrapper | Official SDK is C-only | A pure port is production-proven for you |
| External C service | Isolation beats binary simplicity | Ultra-low latency in-process |
FAQs
Is the syscall package removed?
No - it remains for stdlib compatibility but is deprecated for new application code.
Use x/sys for new low-level calls.
Does x/sys require cgo?
No - it is pure Go with platform-specific files and assembly where needed.
How do I cross-compile with pure Go?
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build works for packages without import "C".
No C cross-compiler required.
When is mattn/go-sqlite3 still right?
When you need specific SQLite compile-time options or extensions only exposed via the C API.
Otherwise evaluate pure drivers first.
Can I mix x/sys and cgo in one module?
Yes - keep syscalls in pure Go packages and isolate cgo to wrappers.
Avoid importing C from hot HTTP handlers.
What about WASM targets?
Syscalls differ entirely on js/wasm and wasip1.
See the WebAssembly overview in this section; cgo is not available there.
Are ioctl and epoll available without cgo?
golang.org/x/sys/unix exposes many of them on Linux and BSDs.
Verify support for your GOOS/GOARCH combination.
How do I find pure-Go replacements?
Search pkg.go.dev with CGO_ENABLED=0 build tags, check Awesome Go lists, and read driver READMEs for cgo requirements.
Does pure Go help security review?
Yes - memory safety of Go applies; fewer native blobs to audit.
C libraries still need review when wrapped.
What if x/sys is missing my syscall?
Contribute to golang.org/x/sys with generated constants from official headers.
Last resort: small cgo package behind internal API.
Can os/exec replace cgo for CLI tools?
Often yes - shelling out to openssl or ffmpeg avoids linking them.
Trade process overhead for build simplicity.
Should libraries default to pure Go?
Idiomatic Go modules ship !cgo builds when feasible and document cgo-only accelerators.
Consumers thank you in CI and cross-compile pipelines.
Related
- CGO: Crossing the Go-C Boundary - When the C bridge is justified
- CGO Basics - Build tags and
CGO_ENABLED - Interop Decision Guide: CGO vs RPC vs Rewrite - Strategy comparison
- FFI Safety: Pointers, C.CString & Free - Risks when cgo is required
- WebAssembly Overview (see WebAssembly & TinyGo section) - Non-cgo deployment paths
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).