Back to Oh My Openagent

Library Defaults — Full Decision Tree (Go 2026)

packages/shared-skills/skills/programming/references/go/libraries.md

4.6.013.7 KB
Original Source

Library Defaults — Full Decision Tree (Go 2026)

The opinionated, in-production stack for 2026 Go. Every entry has a one-line rationale and a canonical snippet so the agent does not relearn each library's idioms.

The biggest difference from Python/Rust/TypeScript: Go has fewer "best" choices and more "boring" choices. The standard library is the default; reach outside it only when the rationale below applies.


HTTP framework — gin (default) or chi (minimalist) or net/http (no deps)

The reality of 2026 Go: gin runs ~48% of new Go API projects (Go Developer Survey 2024 + crawls of new repos), with gorilla/mux (~17%, in maintenance), echo (~16%), and fiber (~11%) the remaining quarter. The skill picks gin not because it is technically superior — it is not — but because:

  1. The ecosystem (middleware, examples, SO answers) is largest.
  2. The CLIProxyAPI codebase, which this skill's backend-stack.md is distilled from, uses gin in production for OpenAI/Gemini/Claude proxying including SSE streaming and WebSocket upgrades. That is real reference code, not a toy.
  3. Gin's Context API is the closest thing Go has to a framework-blessed "request-scoped object", which makes middleware composition straightforward.
go
import "github.com/gin-gonic/gin"

func main() {
    r := gin.New()
    r.Use(gin.Recovery(), middleware.RequestLogger(), middleware.RequestID())
    r.GET("/healthz", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
    _ = r.Run(":8080")
}

Pick chi instead when:

  • You want net/http-compatible handlers (you do, eventually — chi is closer to stdlib).
  • The service is small and you do not need gin's binding helpers.

Pick net/http (stdlib) directly when:

  • The service has fewer than 10 routes and zero auth complexity. Go 1.22's enhanced ServeMux (method+path patterns) eliminated 80% of the historical reason to use a framework.

Never use gorilla/mux (effectively in maintenance), fiber (uses fasthttp which is not stdlib-compatible, so middleware ecosystem is split), or echo (smaller eco than gin, no real advantage today).

See backend-stack.md for the gin canonical layout, middleware ordering, SSE, graceful shutdown, structured logging integration.


RPC — connectrpc/connect-go

The default RPC layer. Use Connect, not raw grpc-go, unless you have a measured reason.

  • Connect is wire-compatible with gRPC AND speaks HTTP/1.1 + HTTP/2 + Connect protocol. One server, three clients (gRPC, gRPC-Web, Connect-Web from browsers).
  • No grpcurl needed for debugging — curl -H "Content-Type: application/json" -d ... works.
  • Streaming, interceptors, deadlines, errors are first-class.
  • Buf toolchain (buf generate, buf lint, buf breaking) for codegen is dramatically nicer than protoc.
go
// Server
mux := http.NewServeMux()
mux.Handle(elizav1connect.NewElizaServiceHandler(&elizaServer{}))
_ = http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))

// Client
client := elizav1connect.NewElizaServiceClient(
    http.DefaultClient,
    "http://localhost:8080",
)
res, err := client.Say(ctx, connect.NewRequest(&elizav1.SayRequest{Sentence: "hi"}))

Use raw grpc-go only when:

  • You need server-streaming-from-multiple-services with a single gRPC mux.
  • You are integrating with a strict gRPC-only environment (Envoy proxy with gRPC reflection, Istio strict-gRPC).

See grpc-connect.md.


Database — pgx/v5 + sqlc + goose

bash
go get github.com/jackc/pgx/v5
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/pressly/goose/v3/cmd/goose@latest
  • pgx/v5 is faster, more type-safe, and has better PostgreSQL feature coverage than database/sql + lib/pq. Use the pgxpool package for connection pooling. Avoid database/sql driver mode — it loses pgx's batch, COPY, listen/notify.
  • sqlc generates type-safe Go from .sql files. Hand-written SQL with hand-written struct mapping is the #1 source of subtle DB bugs. sqlc eliminates the class.
  • goose for migrations — small, command-line first, no global state.

Never use gorm (active record, slow, brings runtime reflection into hot paths, encourages N+1 queries). Never use ent (heavy, opinionated graph layer) unless you specifically want a graph-shaped data model.

See sqlc-pgx.md.


Validation — three layers, three tools

Go has no Pydantic / Zod equivalent and does not need one — but only because you wire three layers properly:

LayerToolPattern
HTTP boundary (gin/chi/net/http)go-playground/validator/v10 via struct tagsbinding:"required,email,min=3"
RPC boundary (protobuf)bufbuild/protovalidate-go(buf.validate.field).string.min_len = 3 in .proto
Domain coreSmart constructor + unexported fieldsNewEmail(s) (Email, error) returns a type whose fields cannot be set from outside
go
// HTTP boundary
type CreateUserReq struct {
    Email    string `json:"email" binding:"required,email"`
    Username string `json:"username" binding:"required,alphanum,min=3,max=32"`
}

// Domain — once a value is of type Email it is provably valid
type Email struct{ raw string }
func NewEmail(s string) (Email, error) {
    if !emailRegex.MatchString(s) { return Email{}, ErrInvalidEmail }
    return Email{raw: strings.ToLower(s)}, nil
}
func (e Email) String() string { return e.raw }

The boundary parses raw input into the domain type once. Inside the domain, no further validation is permitted — the types prove it. This is parse-don't-validate adapted to Go.

See data-modeling.md for the full pattern.


Logging — log/slog (stdlib)

go
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level:     slog.LevelInfo,
    AddSource: true,
}))
slog.SetDefault(logger)

slog.InfoContext(ctx, "request handled",
    slog.String("path", r.URL.Path),
    slog.Int("status", 200),
    slog.Duration("elapsed", elapsed),
)
  • stdlib since 1.21, stable since 1.23. Performance is on par with zerolog for structured output, and faster than logrus by a wide margin.
  • The slog.Handler interface is implemented by all major exporters (OpenTelemetry, Datadog, Honeycomb).
  • The skill bans logrus, zap, zerolog for new code. They are not bad — they are simply superseded. Existing projects on those keep them; new files use slog.

Use the sloglint linter from golangci-strict.md to enforce attr style (slog.String(...) instead of slog.Any(...)).


CLI — cobra + pflag + slog

bash
go install github.com/spf13/cobra-cli@latest
cobra-cli init mytool
cobra-cli add server

cobra is the de facto Go CLI framework — Kubernetes, Docker CLI, Helm, GitHub CLI all use it. The companion viper for config-file-+-env-+-flag merging is optional: prefer caarlos0/env/v11 for env-only configs (12-factor apps), reach for viper only when you genuinely need file-based config.

See cobra-stack.md.


TUI — bubbletea v2 + bubbles v2 + lipgloss v2

Use v2 RC (charm.land/bubbletea/v2), not v1. The v2 model adds:

  • tea.View{Cursor: *tea.Cursor, ...} for real-cursor positioning.
  • SetVirtualCursor(false) on textareas — lets the terminal own the cursor, which is required for CJK IME (Korean Hangul composition, Japanese kana→kanji conversion, Chinese pinyin lookup).
  • Granular mouse events (MouseClickMsg, MouseMotionMsg, MouseReleaseMsg) instead of v1's coarse MouseMsg.

This is not a preference. v1 has no way to position the IME candidate window correctly — Korean input shows up two cells to the left of where you typed, every time. If your TUI accepts text input AND your users include CJK speakers, v1 is broken.

See bubbletea-v2.md for the full IME-correct skeleton.


HTTP client — stdlib + hashicorp/go-retryablehttp

Default: net/http.Client with a tuned http.Transport. The stdlib client is already excellent in 2026 — HTTP/2 by default, connection pooling, sane timeouts when configured.

go
client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        200,
        MaxIdleConnsPerHost: 40,
        IdleConnTimeout:     90 * time.Second,
        DisableCompression:  false,
        ForceAttemptHTTP2:   true,
    },
}

For retry/backoff, add github.com/hashicorp/go-retryablehttp — small, single-purpose, integrates as a wrapper.

Never use resty (too much magic, hides headers, encourages wrong defaults). req is fine but adds dependency surface for marginal benefit over the stdlib + retry wrapper.


JSON — stdlib (default), goccy/go-json (perf), bytedance/sonic (extreme perf)

Stdlib encoding/json improved dramatically in Go 1.21+. Use it.

Reach for goccy/go-json (~3x faster) only when you have measured a hot-path bottleneck:

go
import json "github.com/goccy/go-json"
// drop-in replacement — same API

Reach for bytedance/sonic (~5x faster, requires amd64/arm64) for production proxies with thousands of RPS of JSON traversal. CLIProxyAPI uses tidwall/gjson + tidwall/sjson for partial-tree mutation without full unmarshal — a different optimization, useful when you transform large payloads. See backend-stack.md.


Concurrency primitives — stdlib only

NeedUse
Goroutine group with error propagationgolang.org/x/sync/errgroup
Semaphoregolang.org/x/sync/semaphore
Single-flight dedupgolang.org/x/sync/singleflight
Lazy initsync.OnceValue / sync.OnceFunc (Go 1.21+, replaces sync.Once for typed values)
Atomic counteratomic.Int64 (Go 1.19+, typed atomics — don't use the old func-style)
Channel-based fanoutchan T with errgroup for shutdown

The x/sync packages are stdlib-quality but live outside std. See concurrency.md for the discipline.


Time — stdlib + benbjohnson/clock for tests

go
type Clock interface { Now() time.Time }
// Production
var realClock Clock = clockImpl{}
// Test
fake := clock.NewMock()
fake.Set(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))

Never call time.Now() directly inside domain code. Inject a Clock. Tests become deterministic, no time.Sleep flakiness.


IDs — google/uuid (UUID v4/v7) or xid (sortable short ID)

go
import "github.com/google/uuid"
id := uuid.Must(uuid.NewV7())  // sortable, time-ordered, 128-bit

UUID v7 is the modern default — sortable like v6, random like v4. Use v4 only when leaking creation time is a privacy concern.

For short, URL-safe IDs (~12 bytes, sortable) use rs/xid — Kubernetes-style.


Crypto — stdlib + alecthomas/argon2id for passwords

Stdlib crypto/* for everything. For password hashing, argon2id is the 2026 standard — bcrypt is acceptable but argon2 is OWASP's recommendation since 2023.

go
import "github.com/alecthomas/argon2id"
hash, err := argon2id.CreateHash("password", argon2id.DefaultParams)

Data — apache/arrow-go/v18 + marcboeker/go-duckdb + gonum

Same philosophy as Python's "never pandas":

NeedUse
Tabular over CSV/Parquet/JSONDuckDB-Go bindings — zero-copy Arrow integration
In-memory frameArrow + custom code (Go has no pandas-equivalent and that's fine)
Numericalgonum.org/v1/gonum
Statsgonum/stat

Go's data-science story is intentionally thin. For heavy data work, write the pipeline in Polars/DuckDB (see python/data-processing.md), expose the result via Parquet or Arrow, consume from Go.


Testing — stdlib + selective additions

NeedUse
Assertionsstretchr/testify/require (fail-fast) — assert only in table-driven loops
Snapshots / goldenhexops/autogold/v2 (auto-updates with -update)
Property-basedpgregory.net/rapid (modern) or stdlib testing/quick
Mocksgo.uber.org/mock (gomock successor)
HTTP mocksh2non/gock for outbound, stdlib httptest for inbound
Integration containerstestcontainers/testcontainers-go
Goroutine leakgo.uber.org/goleak
Benchmarksstdlib testing.B + perf.dev/benchstat

See testing.md for canonical patterns.


Config — caarlos0/env/v11

go
type Config struct {
    Port        int           `env:"PORT" envDefault:"8080"`
    DatabaseURL string        `env:"DATABASE_URL,required"`
    Timeout     time.Duration `env:"TIMEOUT" envDefault:"30s"`
}

var cfg Config
if err := env.Parse(&cfg); err != nil { log.Fatal(err) }

Pure 12-factor. Defaults via struct tag, required marker, parsing for time.Duration, slices, maps. Use viper only if you also need file-based config — most services do not.


Choosing an unfamiliar dependency — the checklist

Before go get-ing anything new:

  1. Is it maintained? Latest tag within 12 months? Owner active?
  2. Does it expose stdlib-compatible types (io.Reader, context.Context, http.Handler)? If it invents its own Connection or Request type, that's a yellow flag.
  3. Does it use init() for side effects? REJECT. init() ruins testability.
  4. Does it call log.Fatal / panic outside of true programmer-error paths? REJECT.
  5. Does it have a context.Context first-arg convention? If not, REJECT — cancellation is non-negotiable.
  6. Does adding it overlap with something already in your go.mod? Pick one.

Sources