Deep dive
Decision table
| Job | Pick | Why |
|---|---|---|
| REST/JSON service | net/http (1.22+ routing patterns) or chi | Stdlib is production-grade |
| gRPC | google.golang.org/grpc, or connect-go | First-class codegen |
| Database | database/sql + pgx (Postgres), sqlc for typed queries | Simple, fast |
| Concurrency fan-out with errors | golang.org/x/sync/errgroup | Cancellation on first error |
| CLI | cobra or stdlib flag | Static binary distribution |
| Kubernetes controllers | controller-runtime / kubebuilder | The K8s ecosystem is Go |
| Logging | log/slog (stdlib) | Structured, no dependency |
| Hot CPU kernel | Keep in Go, or call Rust/C via cgo only if big batches | cgo calls cost order of tens of ns and complicate builds |
Memory and performance model
- Escape analysis decides stack vs heap;
go build -gcflags=-mshows why something escapes. Fewer heap allocations = less GC work. - GC cost is proportional to live heap and allocation rate;
GOGC=100(default) lets the heap grow to about 2x live before collecting.GOMEMLIMITsets a soft cap so containers don't OOM. - Interfaces are two words (type, data); calls through them are indirect and block inlining.
- Slices are (pointer, len, cap) views; maps are hash tables with randomized iteration.
- Profiling built in:
pprofCPU, heap, goroutine, mutex and block profiles;go tool trace.
Concurrency model
- M:N scheduler: G (goroutines) on M (OS threads) via P (processors,
GOMAXPROCS, cgroup-aware since Go 1.25), with work stealing and preemption. - Network I/O parks the goroutine and uses epoll/kqueue under the hood; blocking syscalls hand off the P to another thread.
- Channels for ownership transfer and pipelines;
sync.Mutex,sync/atomicfor shared state;context.Contextfor cancellation and deadlines through call chains. - Race detector:
go test -raceinstruments memory accesses; run it in CI.
Code
Service skeleton: routing, timeouts, bounded worker pool, graceful shutdown (Go 1.22+).
package main
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
type Job struct{ ID string }
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case j := <-jobs:
slog.Info("processing", "id", j.ID)
time.Sleep(50 * time.Millisecond) // real work here
}
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
jobs := make(chan Job, 1024) // bounded queue = backpressure
for i := 0; i < 8; i++ {
go worker(ctx, jobs)
}
mux := http.NewServeMux()
mux.HandleFunc("POST /jobs/{id}", func(w http.ResponseWriter, r *http.Request) {
select {
case jobs <- Job{ID: r.PathValue("id")}:
w.WriteHeader(http.StatusAccepted)
default:
http.Error(w, "queue full", http.StatusServiceUnavailable) // shed load, don't block
}
})
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // defaults are "no timeout": slowloris otherwise
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("listen", "err", err)
os.Exit(1)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx) // stop accepting, finish in-flight requests
}
Build:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app . # static, reproducible paths
GOMEMLIMIT=900MiB ./app # in a 1 GiB container
Efficiency
- Cost: a new goroutine gets a few KB of stack, and hundreds of thousands are practical (Go FAQ). GC CPU per cycle is a fixed cost plus a cost per byte of live heap, and more pointers mean more GC work (Go GC guide). A cgo call costs order of tens of ns, far more than a Go call. Bound: allocation rate and live-heap pointers (GC CPU), then memory.
- Levers:
- fewer heap allocations (
go build -gcflags=-m, escape analysis) — less GC work (GC guide) — hot request paths - raise
GOGC— doubling it doubles heap overhead and roughly halves GC CPU (GC guide) — when memory is spare GOMEMLIMIT— soft cap that stops container OOMs (GC guide) — containers with a fixed memory limit- profile-guided optimization — around 2-14 % on Go 1.22 benchmarks (go.dev/doc/pgo) — CPU-bound services with a representative profile
- value slices and indices instead of pointer graphs — fewer pointers to scan (GC guide) — large live heaps
- fewer heap allocations (
- Naive → standard → state of the art:
- allocate per request, default
GOGC=100· GC CPU grows with allocation rate - escape-analysis fixes,
GOGCandGOMEMLIMITtuned frompprof· GC CPU and memory trade set on purpose - the above plus PGO with a production profile · a further 2-14 % CPU
- allocate per request, default
Build and deploy
- Modules (
go.mod,go.sum) with checksum database;go mod tidy,govulncheck ./...for reachable vulnerabilities. - Static binary into
FROM gcr.io/distroless/staticorscratch(add CA certificates). - Fast compiles make large monorepos workable;
go vet,staticcheck,golangci-lintin CI. - PGO: drop a CPU profile as
default.pgoin the main package for order of 2-14 % speedups.
Pitfalls
- Goroutine leaks: goroutines blocked forever on channels nobody reads; always tie them to a context.
http.Client{}andhttp.Server{}without timeouts.- A nil pointer stored in an interface is not a nil interface (
err != nilis true). - Data races on maps: concurrent map writes crash the process; use a mutex or
sync.Mapfor specific patterns. - Sub-slices keep the whole backing array alive;
appendmay alias another slice's memory. deferin long loops accumulates until function return.- Ignoring errors (
_ =) on writes, closes andRows.Err(). - Large heaps of pointer-rich data structures make GC expensive; use value slices and indices.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Go toolchain (gc) | Go | golang/go | brew install go (or the installer from go.dev/dl) | — | — | — | — | compiler, go test -race, pprof, go vet, govulncheck via go install |
| golangci-lint | Go | golangci/golangci-lint | brew install golangci-lint | — | — | — | — | runs many linters in one pass; CI standard |
| staticcheck | Go | dominikh/go-tools | go install honnef.co/go/tools/cmd/staticcheck@latest | — | — | — | — | static analysis beyond go vet |
| pgx | Go | jackc/pgx | go get github.com/jackc/pgx/v5 | — | — | — | — | Postgres driver and toolkit |
| grpc-go | Go | grpc/grpc-go | go get google.golang.org/grpc | — | — | — | — | gRPC for Go |
Sources
- The Go Authors, The Go Programming Language Specification. spec · The Go Memory Model. spec
- The Go Authors, A Guide to the Go Garbage Collector. docs
- Donovan, Kernighan, The Go Programming Language, Addison-Wesley, 2015.
- The Go Authors, Profile-guided optimization. docs
- The Go Authors, Go FAQ (goroutines). docs
- The Go Authors, Go case studies. docs