cgo and Scheduler Transitions (incl. Go 1.26)
cgo and Scheduler Transitions (incl. Go 1.26)
Overview
cgo lets Go call C (and reverse), but each call crosses ABI, stack, and scheduler boundaries. High-frequency cgo is a classic latency and throughput tax.
Go 1.26 runtime work simplified how processors track syscall/cgo blocking: P no longer uses a dedicated syscall state; the scheduler derives blocking from G state. That reduces bookkeeping on hot syscall/cgo paths (community writeups cite meaningful wins especially for cgo-heavy workloads).
Diagram: cgo call path
Go code
│
v
runtime.cgocall
│
v
system stack → C function (may block M)
│
v
return to Go → reacquire P if needed
Call Path (Conceptual)
Go code
-> runtime.cgocall
-> switch to system stack
-> C function runs (may block OS thread)
-> return to Go
-> reacquire P if needed
While C runs, that M is busy in C. The runtime may reassign P so other Go code continues — but threads can accumulate if many Ms stick in C.
Costs
- Call overhead (argument copy, stack switch)
- Loss of some Go compiler visibility
- Harder cross-compilation
- GC interactions with C-allocated memory (you free C memory)
Rules
| Prefer | Avoid |
|---|---|
| Pure Go libraries | cgo in per-request tiny helpers |
| Batch work into fewer cgo calls | Chatty call loops |
| Explicit C resource Close | Relying only on finalizers |
| Build tags for optional cgo | Silent cgo in libraries users don’t expect |
CGO_ENABLED=0 go build ./... # fail if cgo required
go env CGO_ENABLEDDebugging
# see if binary links libc unexpectedly
ldd ./app # Linux
# profiles often show runtime.cgocall
go tool pprof cpu.outRelation To Non-cgo Syscalls
Blocking syscalls from Go also park/reassign; netpoll avoids blocking Ms for sockets. cgo is closer to “unknown blocking foreign code.”
Experiment
# pure Go path baseline
CGO_ENABLED=0 go build -o /tmp/pure .Write a tiny package without cgo and confirm it builds with CGO_ENABLED=0. If you have a cgo dependency, compare:
go test -bench=. -tags=cgo ./...What to notice: Many services never need cgo; forcing CGO_ENABLED=0 in CI catches accidental deps early.
Try next: Read Go 1.26 release notes scheduler section alongside GMP scheduler. For the deployment cost of cgo (libc, Alpine, scratch), start at Go as a Systems Language.