A practical guide to choosing Go packages that remove mechanical work without replacing explicit application architecture.
Updated July 12, 2026.
Go packages should remove mechanical work without taking ownership of your application.
The useful packages handle things such as protocol details, parsing, database drivers, migration ordering, terminal control sequences, retry timing, and test comparison. The dangerous packages redefine your request model, persistence model, error model, lifecycle, or domain types.
This guide is not a list of everything popular on GitHub. It is an opinionated package-selection map for Go services, CLIs, local-first tools, API clients, workers, and developer utilities.
As of July 12, 2026, the current supported Go release line is Go 1.26, with Go 1.26.5 released on July 7, 2026. (Go)
The default rule remains:
Start with the standard library. Add a package when it deletes a bounded mechanical concern without forcing the rest of the application to speak its language.
This is the practical shortlist. The sections below explain when each upgrade earns its place.
| Concern | Default | Upgrade when needed |
|---|---|---|
| HTTP routing | net/http.ServeMux |
Chi for route composition |
| Typed API layer | Plain handlers | Huma for implementation-first OpenAPI |
| OpenAPI codegen | None | oapi-codegen for specification-first APIs |
| WebSockets | coder/websocket | Keep Gorilla in stable existing systems |
| PostgreSQL | pgx/v5 + pgxpool | database/sql adapter for compatibility |
| Stable SQL | sqlc | Scany for flexible scanning |
| Dynamic SQL | Explicit SQL | goqu for conditional construction |
| SQLite | modernc.org/sqlite | mattn/go-sqlite3 when CGO is acceptable |
| Migrations | Goose | golang-migrate for external deployment workflows |
| Environment config | caarlos0/env | Koanf for multiple configuration sources |
| HTTP client | net/http.Client | Resty for many heterogeneous APIs |
| Backoff timing | Explicit bounded loop | cenkalti/backoff |
| Small CLI | flag |
Kong for typed command trees |
| Large CLI | Kong | Cobra for large command ecosystems |
| Interactive prompts | Huh | Bubble Tea for stateful TUIs |
| Logging | slog | Zap or Zerolog after measurement |
| Simple cache | ttlcache | Ristretto for cost-aware high throughput |
| Concurrency | sync + errgroup | conc for structured pools |
| Test comparison | go-cmp | Testify for concise assertions |
| CLI testing | testing package | testscript |
| Goroutine leaks | None | goleak for concurrent services |
| Markdown | Goldmark | Glamour for terminal rendering |
| HTML parsing | goquery | Colly for real crawling |
| Go analysis | x/tools | Tree-sitter for other languages |
| IDs | UUID where required | ULID for sortable application IDs |
| Compression | Standard library | klauspost/compress after measurement |
| LLM clients | Official SDK adapters | Multi-provider adapter behind your interface |
| PostgreSQL jobs | Explicit synchronous work | River |
| Redis jobs | Explicit synchronous work | Asynq |
| Durable workflows | Explicit state machine | Temporal when durability requires it |
A dependency should own one of these:
- A wire protocol.
- A file or document format.
- A database driver.
- A well-defined algorithm.
- An operating-system abstraction.
- Mechanical code generation.
- A reusable infrastructure state machine.
- Terminal or browser protocol mechanics.
Your application should continue to own:
- Domain types.
- State transitions.
- Persistence boundaries.
- Retry eligibility.
- Authorization policy.
- Routing decisions.
- Model selection.
- Evidence ranking.
- Business invariants.
- Error classification.
- Observability semantics.
The distinction matters more than package popularity.
A package that saves 300 lines but requires 3,000 lines of application code to conform to its abstractions is not removing complexity. It is relocating ownership.
Since Go 1.22, http.ServeMux supports method-aware patterns and path variables:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = id
})For small APIs, internal services, webhooks, health endpoints, and local tools, this is enough.
Use net/http when you need:
- Method and path routing.
- Ordinary middleware.
- JSON request and response handling.
- Static files.
- Standard authentication.
- A handful of endpoints.
Do not install a router before ServeMux has demonstrated an actual limitation.
Use github.com/go-chi/chi/v5 when you need:
- Route groups.
- Middleware scoped to subrouters.
- Mountable handlers.
- Cleaner composition across API modules.
- More expressive route organization.
Chi remains fully compatible with net/http, has no external dependencies, and is designed around standard http.Handler composition. (GitHub)
r := chi.NewRouter()
r.Use(requestID)
r.Use(recoverer)
r.Route("/api", func(r chi.Router) {
r.Use(authenticate)
r.Get("/users/{id}", getUser)
r.Post("/users", createUser)
})Chi is the correct upgrade when the problem is route composition.
It is not necessary merely because a project has an HTTP server.
Review github.com/danielgtaylor/huma/v2 when the API contract itself is important.
Huma provides an OpenAPI 3.1 and JSON Schema layer while allowing you to retain your existing router, middleware, logging, and metrics. It supports incremental adoption rather than requiring the application to be rebuilt around a proprietary router. (GitHub)
Use Huma when you want:
- OpenAPI generated from handler definitions.
- Request validation.
- Typed path, query, header, and body inputs.
- Consistent error responses.
- Generated documentation.
- An API contract that stays synchronized with the implementation.
type GetUserInput struct {
ID string `path:"id"`
}
type GetUserOutput struct {
Body User
}
huma.Get(api, "/users/{id}", func(
ctx context.Context,
input *GetUserInput,
) (*GetUserOutput, error) {
user, err := users.Get(ctx, input.ID)
if err != nil {
return nil, err
}
return &GetUserOutput{Body: user}, nil
})Huma is not the default for every HTTP service. It is the upgrade when schema generation and boundary validation materially reduce duplicated work.
Use github.com/oapi-codegen/oapi-codegen/v2 when the OpenAPI document is authoritative.
It can generate:
- Server interfaces.
- Request and response models.
- HTTP clients.
- Strict server wrappers.
- Boilerplate for multiple routers.
The project supports OpenAPI 3.0 and 3.1 and intentionally generates comparatively explicit Go code rather than hiding behavior behind a runtime framework. (GitHub)
Choose based on contract direction:
Go implementation is authoritative
→ Huma
OpenAPI specification is authoritative
→ oapi-codegen
Small internal API without contract distribution
→ net/http or Chi
The experimental oapi-codegen v3 line is not yet the conservative production default. Its own repository describes it as experimental and warns that generated code and command-line options are not stable. (GitHub)
These remain valid frameworks, but they should be deliberate choices rather than defaults.
Use Gin or Echo when:
- The team already knows the framework.
- Existing middleware and conventions are built around it.
- Rapid onboarding matters more than standard-library purity.
- Replacing it would produce no meaningful architectural gain.
Use Fiber only when:
- Fasthttp compatibility is acceptable.
- You have measured a transport-level performance requirement.
- You accept a separate middleware and request/response ecosystem.
Do not migrate a functioning service merely to reach a theoretically cleaner router. Framework switching cost touches handlers, middleware, tests, request contexts, error handling, and instrumentation.
For new WebSocket code, review:
github.com/coder/websocketIt provides an idiomatic, context-aware API and works with net/http.
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
defer conn.CloseNow()
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
if err := wsjson.Write(ctx, conn, payload); err != nil {
return
}Keep gorilla/websocket in existing systems where it is stable and well tested. A working protocol implementation does not need to be replaced merely because a newer API is cleaner.
The migration case is strongest when new code benefits from context cancellation, simpler JSON helpers, and reduced protocol-management boilerplate.
For PostgreSQL applications, use:
github.com/jackc/pgx/v5
github.com/jackc/pgx/v5/pgxpoolPgx is a pure-Go PostgreSQL driver and toolkit. Its native API exposes PostgreSQL-specific features such as COPY and LISTEN/NOTIFY, while also providing a database/sql adapter. (GitHub)
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return err
}
defer pool.Close()Prefer the native pgx interface when:
- The application is PostgreSQL-only.
- You use arrays, JSONB, ranges, enums, or custom types.
- You need
COPY. - You need
LISTEN/NOTIFY. - You want pgx batching or native pooling.
Use the database/sql adapter when a library requires it or when database portability is a real requirement.
For new PostgreSQL code, there is little reason to begin with lib/pq.
Use sqlc when queries are known at build time.
You write SQL:
-- name: GetUser :one
SELECT id, email, created_at
FROM users
WHERE id = $1;Sqlc generates typed Go:
func (q *Queries) GetUser(
ctx context.Context,
id int64,
) (User, error)Current sqlc documentation classifies Go support for PostgreSQL and MySQL as stable, while SQLite support remains beta. (sqlc Documentation)
Sqlc is strongest when:
- SQL is part of the application design.
- Queries are mostly static.
- Compile-time parameter and result typing is valuable.
- You want explicit SQL without runtime ORM behavior.
- Database changes should break generation or compilation before deployment.
Sqlc also understands migration directories from tools including Goose, Atlas, dbmate, golang-migrate, and tern. (sqlc Documentation)
Sqlc should be considered the default for DB-heavy PostgreSQL services.
Use:
github.com/georgysavva/scany/v2when you want typed struct scanning without committing all queries to code generation.
Scany is useful for:
- Reporting queries.
- Dynamic selects.
- Admin tools.
- One-off joins.
- Queries assembled by another package.
- Gradual migration from manual
Scancalls.
var users []User
err := pgxscan.Select(
ctx,
pool,
&users,
`SELECT id, email FROM users WHERE active = true`,
)Scany is a better focused recommendation than treating sqlx as mandatory.
Use:
github.com/doug-martin/goqu/v9for genuinely dynamic SQL:
- Optional filters.
- Conditional joins.
- User-defined sorting.
- Search builders.
- Bulk inserts.
- Programmatically assembled predicates.
query, args, err := goqu.
From("users").
Select("id", "email").
Where(goqu.Ex{
"status": "active",
}).
ToSQL()Do not construct static SQL through a builder. Static SQL is more legible as SQL.
A useful division is:
Stable application queries
→ sqlc
Dynamic query construction
→ goqu
Dynamic or ad hoc result scanning
→ scany
PostgreSQL transport and pooling
→ pgx
Sqlx remains useful and mature. It adds struct scanning, named parameters, and IN expansion to database/sql.
Use it when:
- The application already uses
database/sql. - Code generation is undesirable.
- Portability across SQL drivers is important.
- The team already understands and maintains it.
Do not treat sqlx as an automatic dependency. Pgx, sqlc, scany, and goqu now produce a more precise package palette for PostgreSQL-first systems.
Use GORM when rapid CRUD development, conventions, hooks, associations, and ORM familiarity outweigh the need for transparent SQL.
Avoid it when:
- Query plans matter.
- SQL behavior must be obvious from the source.
- Domain persistence is not naturally CRUD-shaped.
- Implicit callbacks and association behavior create uncertainty.
Use Ent when:
- The domain is relationship-heavy.
- Schema-as-Go is desirable.
- Generated graph traversal has real value.
- The team accepts generated persistence APIs as an architectural layer.
Review Bob when you want a database-first generated query layer with stronger dynamic-query capabilities than sqlc.
The correct question is not “Which ORM is best?”
It is:
What should be authoritative?
SQL queries
→ sqlc
Database schema
→ Bob or another schema-driven generator
Go-defined entity graph
→ Ent
Runtime ORM conventions
→ GORM
Use:
modernc.org/sqlitewhen you need:
- CGO-free builds.
- Straightforward cross-compilation.
- Local-first applications.
- Single-binary CLI tools.
- Embedded databases in workers or desktop utilities.
Use github.com/mattn/go-sqlite3 when CGO is acceptable and you specifically need its ecosystem, extension behavior, or performance profile.
Do not choose solely from generic benchmarks. Test your workload:
- Concurrent readers.
- Write transaction duration.
- FTS5 queries.
- JSON operations.
- Bulk inserts.
- WAL behavior.
- Startup time.
- Binary size.
The driver is only part of SQLite correctness. You still need explicit ownership of:
- Busy timeout.
- WAL mode.
- Transaction boundaries.
- Connection pool size.
- Checkpoint behavior.
- Migration ordering.
- Backup and restore.
For local applications, begin by considering:
db.SetMaxOpenConns(1)Then increase concurrency only after the transaction model requires it and has been tested.
Use:
github.com/pressly/goose/v3Goose is both a CLI and a library. It supports SQL migrations, Go migration functions, embedded migrations, out-of-order migrations, data seeding, and multiple databases including PostgreSQL, MySQL, SQLite, ClickHouse, and MSSQL. (GitHub)
//go:embed migrations/*.sql
var migrations embed.FS
func migrate(db *sql.DB) error {
goose.SetBaseFS(migrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
return goose.Up(db, "migrations")
}Goose fits Go applications particularly well when migrations ship inside the binary.
Use golang-migrate/migrate when:
- Migrations are executed independently from the Go application.
- You need one of its many source or database drivers.
- Infrastructure tooling already standardizes on it.
- Cross-language deployment workflows matter.
Neither package is universally superior.
The choice is:
Go application owns and embeds migrations
→ Goose
External deployment system owns migrations
→ golang-migrate
Configuration has separate concerns:
- Decode values.
- Merge sources.
- Validate the resulting application configuration.
Do not use one package merely because it claims to do all three.
Use:
github.com/caarlos0/env/v11It is a zero-dependency package for decoding environment variables into structs and supports generic parsing through ParseAs. (GitHub)
type Config struct {
Port int `env:"PORT" envDefault:"8080"`
DatabaseURL string `env:"DATABASE_URL,required"`
Debug bool `env:"DEBUG" envDefault:"false"`
}
cfg, err := env.ParseAs[Config]()
if err != nil {
return err
}It intentionally does not load .env files. Load those separately during local development when required. (GitHub)
This separation is useful:
.env file loading
→ development concern
environment decoding
→ caarlos0/env
domain validation
→ Config.Validate()
Use:
github.com/knadh/koanf/v2when configuration genuinely comes from multiple sources:
- Defaults.
- Files.
- Environment variables.
- Command-line overrides.
- Remote stores.
- Nested maps.
Koanf supports modular providers and parsers for formats and sources including JSON, TOML, YAML, environment variables, files, and remote storage. (GitHub)
Prefer Koanf over Viper when you want more explicit source composition and less global behavior.
Use Viper when:
- The project already depends on the broader Cobra ecosystem.
- Live reload is required.
- Existing conventions and integrations make it cheaper than replacement.
Do not install it for three environment variables.
Configuration decoding is not domain validation.
func (c Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.DatabaseURL == "" {
return errors.New("database URL is required")
}
return nil
}Keep invariants visible in Go code when possible.
The standard client is sufficient for most API integrations.
Create and reuse one configured client:
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
},
}Do not create a new client per request.
Set:
- Overall timeout.
- Transport limits.
- TLS behavior.
- Proxy behavior.
- Redirect policy.
- Instrumentation.
Use:
github.com/go-resty/resty/v2when you maintain several external API integrations and repeatedly need:
- Request and response middleware.
- Consistent authentication setup.
- Typed JSON helpers.
- Multipart requests.
- Retry hooks.
- Debugging support.
- Method-chaining ergonomics.
Resty should be an adapter implementation detail, not a domain contract.
Use a maintained version of:
github.com/cenkalti/backofffor retry timing and backoff calculations.
But keep retry eligibility in your code.
A backoff package can decide when the next attempt occurs. It cannot know whether an operation is safe to retry.
Your policy must consider:
- HTTP method.
- Body replayability.
- Idempotency keys.
- Provider error codes.
- Rate-limit headers.
- Context deadline.
- Maximum elapsed time.
- Whether the operation may have succeeded remotely.
type RetryDecision struct {
Retry bool
After time.Duration
Reason string
}Do not hide this behind a generic “retry everything” transport.
CLI packages solve different scales of problem.
Use the standard flag package when the application has:
- One command.
- A few flags.
- No nested command tree.
- No special help or completion requirements.
It remains the smallest and most durable option.
Use:
github.com/alecthomas/kongfor most medium-sized CLIs.
Kong models the command tree as structs:
type CLI struct {
Verbose bool `short:"v"`
Index IndexCmd `cmd:"" help:"Index a repository."`
Query QueryCmd `cmd:"" help:"Query the index."`
}This is a strong fit when you prefer:
- Declarative structure.
- Typed arguments.
- Minimal registration code.
- Explicit command ownership.
- Less scaffolding than Cobra.
For compact developer tools, Kong is often the best default.
Use:
github.com/spf13/cobrawhen the CLI needs:
- Many nested subcommands.
- Shell completions.
- Generated documentation.
- A plugin-like command hierarchy.
- Familiar conventions for contributors.
Cobra is appropriate for kubectl-shaped tools. It is usually excessive for a five-command utility.
Review:
github.com/peterbourgon/ff/v3when a CLI or daemon needs deterministic composition of:
- Command-line flags.
- Environment variables.
- Configuration files.
It augments the standard flag model without turning configuration into an application framework.
Use:
github.com/charmbracelet/huh/v2for prompts, forms, selections, confirmations, and guided terminal setup.
var project string
var confirm bool
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Project name").
Value(&project),
huh.NewConfirm().
Title("Create project?").
Value(&confirm),
),
)Use Huh when the interface is form-shaped.
Use:
github.com/charmbracelet/bubbletea
github.com/charmbracelet/bubbles
github.com/charmbracelet/lipglosswhen the terminal program has:
- Persistent state.
- Keyboard-driven navigation.
- Asynchronous events.
- Multiple panels or views.
- Live updates.
- Long-running interaction.
Do not build a Bubble Tea application to ask three setup questions. Use Huh.
The terminal stack becomes:
Flags and arguments
→ flag, Kong, or Cobra
Prompts and forms
→ Huh
Full-screen stateful TUI
→ Bubble Tea
Styling and layout
→ Lip Gloss
Reusable widgets
→ Bubbles
Use log/slog for new projects.
logger := slog.New(
slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}),
)
logger.Info(
"request completed",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"duration", elapsed,
)Slog gives you:
- Structured fields.
- Log levels.
- Context-aware logging.
- Replaceable handlers.
- A standard application-facing API.
Use Zap or Zerolog when:
- They already exist in the system.
- Their ecosystem integrations are important.
- Profiling shows logging overhead is material.
- Their APIs match an established operational stack.
There is no reason to migrate a stable logger merely to standardize on slog.
Use OpenTelemetry when traces, metrics, and logs must share request and operation context across process boundaries.
The important package boundary is not “put OpenTelemetry everywhere.”
It is:
Application code
→ records domain-relevant operations
Instrumentation adapters
→ translate them into traces and metrics
Exporter configuration
→ remains outside domain packages
Review:
github.com/XSAM/otelsqlfor database/sql instrumentation.
It can provide query spans, errors, connection-pool metrics, and latency observations without forcing repositories to manually wrap every call.
For native pgx, use pgx-native tracing hooks rather than routing through database/sql solely to obtain instrumentation.
A cache must have explicit semantics.
Before choosing a package, define:
- Key identity.
- Maximum memory.
- Entry cost.
- TTL meaning.
- Whether stale values are acceptable.
- Whether admission may reject entries.
- Whether eviction order must be deterministic.
- Whether cache misses may stampede.
Use:
github.com/jellydator/ttlcache/v3for:
- Expiring API results.
- Session-like local state.
- Small metadata caches.
- Straightforward TTL behavior.
Use:
github.com/dgraph-io/ristretto/v2for:
- Embeddings.
- Parsed syntax trees.
- Large documents.
- Compiled templates.
- Variable-sized values.
- High-throughput concurrent workloads.
Ristretto is preferable when an entry’s memory cost matters more than its count.
Use:
github.com/hashicorp/golang-lru/v2only when strict or understandable LRU-style behavior is actually the requirement.
Do not select LRU merely because it is the cache algorithm you remember.
A useful ownership split is:
Simple expiration
→ ttlcache
Memory-budgeted, high-throughput cache
→ Ristretto
Strict recency-based eviction
→ golang-lru
Cross-process shared cache
→ Redis
Use the standard library first:
sync.Mutexsync.RWMutexsync.Oncesync.WaitGroupsync.Maponly for its documented use cases- channels when ownership transfer or signaling is the actual model
Use:
golang.org/x/sync/errgroupwhen sibling goroutines share cancellation and one failure should stop the group.
group, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
group.Go(func() error {
return process(ctx, item)
})
}
if err := group.Wait(); err != nil {
return err
}Use:
github.com/sourcegraph/concwhen its typed pools, panic handling, and structured goroutine helpers remove repeated lifecycle code.
Do not add it merely to avoid writing go func().
Use:
github.com/sony/gobreakerwhen repeated calls to a failing dependency should be rejected temporarily.
A circuit breaker is not a retry package.
Retry
→ repeat one operation under bounded conditions
Circuit breaker
→ stop beginning new operations against an unhealthy dependency
Keep failure classification and dependency health semantics in your application.
Use:
- Table-driven tests.
- Subtests.
t.Helper().t.Cleanup().- Fuzz tests.
- Benchmarks.
- Real temporary directories through
t.TempDir(). - Integration tests against real boundaries when practical.
Third-party packages should improve failure information or remove mechanical setup.
Use:
github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/cmpoptsGo-cmp is a more powerful and safer semantic-comparison tool than reflect.DeepEqual. (GitHub)
if diff := cmp.Diff(want, got, cmpopts.EquateEmpty()); diff != "" {
t.Fatalf("result mismatch (-want +got):\n%s", diff)
}It is especially useful for:
- Nested structs.
- Ignoring generated fields.
- Comparing errors.
- Approximate floating-point values.
- Sorting unordered slices before comparison.
- Custom equivalence.
Use Testify selectively:
github.com/stretchr/testify/require
github.com/stretchr/testify/assertA reasonable policy is:
Setup preconditions
→ require
Multiple independent checks
→ assert
Complex structural values
→ go-cmp
Avoid building tests around giant assertion chains.
Use:
github.com/rogpeppe/go-internal/testscriptfor CLI tools.
Testscript provides filesystem-based script tests and was still actively documented and updated in April 2026. (Go Packages)
exec tool index ./fixture
stdout 'indexed 14 files'
exists state.db
exec tool query symbol
stdout 'internal/query'
It is one of the most useful under-adopted packages for Go CLI developers because it tests the actual command surface, process output, files, environment, and exit status together.
Use:
go.uber.org/goleakfor services, workers, and concurrency-heavy packages.
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}It catches goroutines that survive test completion and would otherwise accumulate silently in long-running processes.
Prefer:
- Small interfaces.
- Handwritten fakes.
- In-memory implementations.
- Real test databases.
- HTTP test servers.
- Deterministic clocks and ID sources.
Generate mocks only when the interface is large, externally owned, or expensive to fake manually.
Mocking should replace an uncontrollable boundary, not every concrete collaborator.
Use:
github.com/fsnotify/fsnotifyfor cross-platform filesystem notifications.
Keep your own higher-level policy:
- Debouncing.
- Recursive directory ownership.
- Rename handling.
- Event coalescing.
- Full rescan fallback.
- Ignore patterns.
Fsnotify owns OS event mechanics. It does not own your synchronization model.
Use:
github.com/bmatcuk/doublestar/v4for Git-style recursive globbing, particularly ** patterns.
This is preferable to maintaining custom glob semantics.
Use:
github.com/PuerkitoBio/goqueryfor DOM querying and extraction.
Use Colly when the problem expands into a crawler with:
- Request scheduling.
- Visit rules.
- Domain limits.
- Callback pipelines.
- Crawl concurrency.
A monitor or one-page extractor usually needs goquery, not a crawler framework.
Use the official extended tooling:
golang.org/x/tools/go/packages
golang.org/x/tools/go/ssa
golang.org/x/tools/go/callgraph
golang.org/x/tools/go/types/typeutil
golang.org/x/tools/go/ast/astutilUse these for:
- Package-aware loading.
- Build tags.
- Module resolution.
- Type information.
- Method sets.
- SSA.
- Call graphs.
- Definitions and references.
Use Tree-sitter for multi-language syntax extraction.
Semantic Go analysis
→ go/packages, go/types, SSA
Multi-language structural parsing
→ Tree-sitter
Unified repository graph
→ your own canonical symbol and edge model
Do not use a syntax-only parser to recreate semantic information that the Go toolchain already exposes.
Use:
github.com/yuin/goldmarkGoldmark is the standard choice when you need:
- CommonMark-compatible parsing.
- Extension support.
- AST transformations.
- Custom renderers.
- Predictable Markdown processing.
Use:
github.com/charmbracelet/glamourfor displaying Markdown in terminal applications.
It is a renderer, not your canonical Markdown parser or document storage format.
Use:
github.com/JohannesKaufmann/html-to-markdown/v2for readable extraction pipelines, clipping tools, crawler output, and LLM context preparation.
Keep normalization policy outside the converter:
- Elements to discard.
- Link handling.
- Image handling.
- Whitespace rules.
- Maximum depth.
- Repeated navigation removal.
Use the standard regexp package unless you specifically need features it does not support, such as lookbehind or backreferences.
Use:
github.com/dlclark/regexp2only for those compatibility requirements.
A more powerful regex engine also permits more expensive patterns. Treat untrusted patterns as potentially hostile input.
Use:
github.com/google/uuidwhen interoperability requires UUIDs.
Use:
github.com/oklog/ulid/v2when identifiers should be:
- Time sortable.
- Decentralized.
- Compact enough for logs and URLs.
- Suitable for database ordering.
Good uses include:
- Tasks.
- Events.
- Observations.
- Messages.
- Local-first records.
- Append-only ledger entries.
Use:
github.com/zeebo/xxh3or:
github.com/cespare/xxhash/v2for:
- Cache keys.
- Content fingerprints.
- Duplicate detection.
- Chunk identity.
- Repository snapshots.
Do not use non-cryptographic hashes for:
- Passwords.
- Authentication.
- Signatures.
- Adversarial integrity validation.
Use crypto/sha256, HMAC, or an appropriate cryptographic primitive for security-sensitive identity.
Use:
github.com/klauspost/compresswhen compression performance or format support exceeds what the standard library provides.
It is useful for:
- High-throughput gzip.
- Zstandard.
- Large archives.
- Network compression.
- Parallel workloads.
Use the standard library when compression is not a measured bottleneck. A dependency is not justified merely because its benchmark is faster.
Use official provider SDKs behind narrow internal adapters:
github.com/openai/openai-go
github.com/anthropics/anthropic-sdk-go
Provider SDKs should own:
- Authentication.
- Request serialization.
- Streaming protocol details.
- Tool-call wire formats.
- Provider error decoding.
Your code should own:
- Canonical messages.
- Tool definitions.
- Model capabilities.
- Cost and token policy.
- Retry eligibility.
- Fallback routing.
- Provider health.
- Logging and observability.
- Domain-level result types.
type Provider interface {
Generate(
ctx context.Context,
request Request,
) (Response, error)
}Do not allow provider SDK types to spread into application services.
A multi-provider package such as any-llm-go can reduce adapter code, but it should not become the owner of your model-routing policy.
Use agent or chain frameworks only after you have an actual recurring orchestration shape. A direct provider adapter plus explicit Go state machine is often easier to understand and maintain.
Different asynchronous mechanisms should not be collapsed into one homemade queue.
Use River when PostgreSQL already owns application state and a job must be committed atomically with domain changes.
Example:
INSERT document
INSERT indexing job
COMMIT
Either both become visible or neither does.
This avoids maintaining a custom transactional outbox and leasing system for ordinary jobs.
Use Asynq for:
- Delayed work.
- Retryable jobs.
- Queue prioritization.
- Worker concurrency.
- Scheduled work.
- Redis-backed background processing.
Good examples:
- Fetch a URL.
- Generate embeddings.
- Send an email.
- Process a file.
- Rebuild an index.
Do not use a job queue to model agent negotiation, ownership claims, handoffs, or distributed presence. Those are application protocols.
Use Temporal only when work genuinely needs:
- Long-running durable state.
- Timers that survive restarts.
- Human approval.
- Compensation.
- Multi-stage recovery.
- Workflow history.
- Resumption across process or machine failure.
Temporal is an operational platform, not a replacement for a goroutine or a simple queue.
Use Taskfile when you want:
- Cross-platform project tasks.
- Declarative dependencies.
- Environment setup.
- Clear command discovery.
- Less shell portability work.
Use Make when:
- The project already has a good Makefile.
- The team knows it.
- Tasks are small and Unix-oriented.
- File dependency semantics are useful.
Do not migrate a working build system for aesthetic reasons.
Use Mage when build logic genuinely benefits from Go:
- Complex conditional behavior.
- Reusable typed helpers.
- API interactions.
- Cross-platform filesystem work.
- Substantial control flow.
Do not compile a Go task runner to wrap three go test commands.
Current Go versions support tracking tool dependencies through the module rather than relying only on globally installed binaries.
Pin generators, linters, and migration tools. Do not make reproducible builds depend on whatever @latest installed on a developer machine last month.
This does not mean these packages are bad. It means their adoption cost is structural.
They affect:
- Handler signatures.
- Middleware.
- Request contexts.
- Error handling.
- Validation.
- Testing.
- Instrumentation.
Adopt intentionally.
They affect:
- Domain models.
- Persistence APIs.
- Transactions.
- Query behavior.
- Schema ownership.
- Testing strategy.
Evaluate against actual persistence requirements.
Manual constructors remain the default:
store := NewStore(db)
service := NewService(store, logger)
server := NewServer(service, logger)Use generated or runtime DI only when the dependency graph is large enough that manual wiring is a recurring source of errors.
A generic repository often erases database capabilities and replaces explicit SQL with a lowest-common-denominator CRUD interface.
Prefer a small domain-specific store:
type UserStore interface {
Get(ctx context.Context, id UserID) (User, error)
FindActive(ctx context.Context, limit int) ([]User, error)
Save(ctx context.Context, user User) error
}Do not force every table through Repository[T].
Retries require operation knowledge. A transport-level package rarely knows whether a request can safely repeat.
Pass immutable configuration into constructors. Avoid packages that make configuration retrieval an ambient runtime dependency.
The standard library gives you the default architecture and interoperability surface.
Add a dependency only after identifying the missing primitive.
Good package boundaries include:
http.Handlercontext.Contextdatabase/sqlio.Readerio.Writerfs.FSerror
A package that accepts and returns standard interfaces is easier to introduce, test, and remove.
Ask:
How many application files change if this package is removed?
A parser may touch one adapter.
A framework may touch every handler.
Let a package calculate backoff.
Do not let it decide what is safe to retry.
Let a cache evict entries.
Do not let it define identity or freshness.
Let a queue lease work.
Do not let it define your task semantics.
Code generation is often acceptable when:
- The output is readable.
- The generated boundary is stable.
- Generation is deterministic.
- Generated code can be inspected and debugged.
- The generator is pinned.
Sqlc and oapi-codegen are strong because their output is ordinary Go.
Do not use three CLI parsers, four caches, two migration systems, and multiple tokenizers in one application without explicit boundaries.
Experiments are useful. Production ownership should converge.
Low commit frequency can mean:
- Stable and complete.
- Abandoned.
- Awaiting a rewrite.
- Maintained through compatibility patches only.
Inspect:
- Recent releases.
- Open security issues.
- Maintainer responses.
- Supported Go versions.
- Downstream usage.
- Release discipline.
- Whether the API surface is already complete.
A small API can still import a large ecosystem.
Check:
go mod graph
go mod why -m module/path
go list -deps ./...Dependency count is not automatically bad, but it should be intentional.
Confirm:
- Repository owner.
- Exact module path.
- Major-version suffix.
- License.
- Release tags.
- Whether the project moved organizations.
- Whether similarly named modules are unrelated.
Supply-chain attacks increasingly exploit lookalike or repackaged Go modules, so exact module identity matters. Recent research found thousands of malicious repackaged module versions and demonstrated that removed repositories can remain retrievable through module proxies until remediated. (arXiv)
Pin build-time tools and use controlled dependency updates.
Run:
go mod tidy
go mod verify
govulncheck ./...Review dependency upgrades like code changes, especially for packages that own parsing, authentication, serialization, database access, or network protocols.
Your router, PostgreSQL driver, UUID parser, and Markdown engine should be boring.
Your product’s ranking, routing, synchronization, state transitions, and domain semantics may legitimately be custom.
A new package must purchase something:
- Fewer failure modes.
- Less code.
- Better correctness.
- Better observability.
- Better protocol support.
- Better portability.
- Reduced operational burden.
- A cleaner ownership boundary.
“Newer” is not itself a payoff.
The ideal Go dependency graph is not empty.
It is narrow and legible.
Each package should own a mechanism with a clear boundary:
Chi
owns route matching and composition
pgx
owns PostgreSQL protocol and pooling
sqlc
owns SQL-to-Go code generation
Goose
owns migration ordering
caarlos0/env
owns environment decoding
Kong
owns CLI parsing
Huh
owns terminal form mechanics
Ristretto
owns cache admission and eviction
go-cmp
owns semantic test comparison
testscript
owns CLI test execution mechanics
Your application still owns:
state
policy
identity
transactions
invariants
authorization
routing decisions
failure classification
domain observability
That is the useful definition of an awesome Go package:
It removes maintenance without becoming the architecture.
Awesome Go Packages: 2026 Index
This index tracks the current recommendations in the article.
HTTP and APIs
net/httpPostgreSQL and SQL
SQLite and Migrations
Configuration and HTTP Clients
CLI and Terminal UI
Logging, Observability, and Caching
Concurrency and Testing
Parsing, Documents, and Analysis
IDs, Compression, and Durable Work
The standard library remains the default where it already owns the concern cleanly:
net/http,flag,slog,sync,testing, regular expressions, and compression.