Skip to content

Instantly share code, notes, and snippets.

@garyblankenship
Last active July 12, 2026 21:17
Show Gist options
  • Select an option

  • Save garyblankenship/1485968a779a62ae00162fa5a568304e to your computer and use it in GitHub Desktop.

Select an option

Save garyblankenship/1485968a779a62ae00162fa5a568304e to your computer and use it in GitHub Desktop.
Awesome Go Packages #go

Awesome Go Packages: An Opinionated 2026 Guide

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.


The 2026 default package palette

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

The package-selection model

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.


HTTP servers and routing

Default: net/http

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.

Upgrade: Chi

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.

API contracts: Huma

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.

Specification-first APIs: oapi-codegen

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)

Gin, Echo, and Fiber

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.


WebSockets

Default review choice: coder/websocket

For new WebSocket code, review:

github.com/coder/websocket

It 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.


PostgreSQL

Default driver: pgx

For PostgreSQL applications, use:

github.com/jackc/pgx/v5
github.com/jackc/pgx/v5/pgxpool

Pgx 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.

Stable SQL: sqlc

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.

Flexible scanning: scany

Use:

github.com/georgysavva/scany/v2

when 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 Scan calls.
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.

Dynamic SQL: goqu

Use:

github.com/doug-martin/goqu/v9

for 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

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.

ORMs

GORM

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.

Ent

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.

Bob

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

SQLite

Pure Go: modernc.org/sqlite

Use:

modernc.org/sqlite

when 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.


Database migrations

Default for Go-owned applications: Goose

Use:

github.com/pressly/goose/v3

Goose 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 when ecosystem breadth matters

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

Configuration has separate concerns:

  1. Decode values.
  2. Merge sources.
  3. Validate the resulting application configuration.

Do not use one package merely because it claims to do all three.

Environment-only configuration: caarlos0/env

Use:

github.com/caarlos0/env/v11

It 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()

Multi-source configuration: Koanf

Use:

github.com/knadh/koanf/v2

when 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.

Viper

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.

Validate explicitly

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.


HTTP clients

Default: net/http.Client

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.

Resty

Use:

github.com/go-resty/resty/v2

when 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.

Retry behavior: backoff

Use a maintained version of:

github.com/cenkalti/backoff

for 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 applications

CLI packages solve different scales of problem.

Small commands: flag

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.

Struct-driven CLIs: Kong

Use:

github.com/alecthomas/kong

for 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.

Large command ecosystems: Cobra

Use:

github.com/spf13/cobra

when 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.

Configuration precedence: ff

Review:

github.com/peterbourgon/ff/v3

when 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.

Interactive forms: Huh

Use:

github.com/charmbracelet/huh/v2

for 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.

Stateful terminal applications: Bubble Tea

Use:

github.com/charmbracelet/bubbletea
github.com/charmbracelet/bubbles
github.com/charmbracelet/lipgloss

when 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

Logging and observability

Default logger: slog

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.

OpenTelemetry

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

SQL instrumentation: otelsql

Review:

github.com/XSAM/otelsql

for 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.


Caching

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.

Simple TTL cache: ttlcache

Use:

github.com/jellydator/ttlcache/v3

for:

  • Expiring API results.
  • Session-like local state.
  • Small metadata caches.
  • Straightforward TTL behavior.

Cost-aware concurrent cache: Ristretto

Use:

github.com/dgraph-io/ristretto/v2

for:

  • 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.

LRU

Use:

github.com/hashicorp/golang-lru/v2

only 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

Concurrency and resilience

Start with sync and errgroup

Use the standard library first:

  • sync.Mutex
  • sync.RWMutex
  • sync.Once
  • sync.WaitGroup
  • sync.Map only for its documented use cases
  • channels when ownership transfer or signaling is the actual model

Use:

golang.org/x/sync/errgroup

when 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
}

conc

Use:

github.com/sourcegraph/conc

when its typed pools, panic handling, and structured goroutine helpers remove repeated lifecycle code.

Do not add it merely to avoid writing go func().

Circuit breakers

Use:

github.com/sony/gobreaker

when 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.


Testing

Standard testing remains the foundation

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.

Structural comparison: go-cmp

Use:

github.com/google/go-cmp/cmp
github.com/google/go-cmp/cmp/cmpopts

Go-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.

Assertions: Testify

Use Testify selectively:

github.com/stretchr/testify/require
github.com/stretchr/testify/assert

A reasonable policy is:

Setup preconditions
    → require

Multiple independent checks
    → assert

Complex structural values
    → go-cmp

Avoid building tests around giant assertion chains.

CLI integration tests: testscript

Use:

github.com/rogpeppe/go-internal/testscript

for 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.

Goroutine leaks: goleak

Use:

go.uber.org/goleak

for 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.

Mocks

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.


File systems, parsing, and code analysis

Filesystem events: fsnotify

Use:

github.com/fsnotify/fsnotify

for 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.

Glob matching: doublestar

Use:

github.com/bmatcuk/doublestar/v4

for Git-style recursive globbing, particularly ** patterns.

This is preferable to maintaining custom glob semantics.

HTML: goquery

Use:

github.com/PuerkitoBio/goquery

for 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.

Go source analysis

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/astutil

Use 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.


Text, Markdown, and document processing

Markdown: Goldmark

Use:

github.com/yuin/goldmark

Goldmark is the standard choice when you need:

  • CommonMark-compatible parsing.
  • Extension support.
  • AST transformations.
  • Custom renderers.
  • Predictable Markdown processing.

Terminal Markdown: Glamour

Use:

github.com/charmbracelet/glamour

for displaying Markdown in terminal applications.

It is a renderer, not your canonical Markdown parser or document storage format.

HTML to Markdown

Use:

github.com/JohannesKaufmann/html-to-markdown/v2

for 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.

Regular expressions

Use the standard regexp package unless you specifically need features it does not support, such as lookbehind or backreferences.

Use:

github.com/dlclark/regexp2

only for those compatibility requirements.

A more powerful regex engine also permits more expensive patterns. Treat untrusted patterns as potentially hostile input.


IDs and hashing

UUID

Use:

github.com/google/uuid

when interoperability requires UUIDs.

ULID

Use:

github.com/oklog/ulid/v2

when 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.

Fast non-cryptographic hashing

Use:

github.com/zeebo/xxh3

or:

github.com/cespare/xxhash/v2

for:

  • 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.


Compression

Use:

github.com/klauspost/compress

when 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.


AI and LLM clients

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.


Task queues and durable work

Different asynchronous mechanisms should not be collapsed into one homemade queue.

PostgreSQL jobs: River

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.

Redis jobs: Asynq

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.

Durable workflows: Temporal

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.


Task runners and developer tooling

Taskfile

Use Taskfile when you want:

  • Cross-platform project tasks.
  • Declarative dependencies.
  • Environment setup.
  • Clear command discovery.
  • Less shell portability work.

Make

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.

Mage

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.

Pin tools with Go’s tool management

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.


Packages to avoid adding casually

This does not mean these packages are bad. It means their adoption cost is structural.

Full web frameworks

They affect:

  • Handler signatures.
  • Middleware.
  • Request contexts.
  • Error handling.
  • Validation.
  • Testing.
  • Instrumentation.

Adopt intentionally.

ORMs

They affect:

  • Domain models.
  • Persistence APIs.
  • Transactions.
  • Query behavior.
  • Schema ownership.
  • Testing strategy.

Evaluate against actual persistence requirements.

Dependency-injection frameworks

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.

Generic repository packages

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].

Generic retry transports

Retries require operation knowledge. A transport-level package rarely knows whether a request can safely repeat.

Global configuration objects

Pass immutable configuration into constructors. Avoid packages that make configuration retrieval an ambient runtime dependency.


Twelve rules for selecting Go packages

1. Start with the standard library

The standard library gives you the default architecture and interoperability surface.

Add a dependency only after identifying the missing primitive.

2. Prefer packages that preserve standard interfaces

Good package boundaries include:

  • http.Handler
  • context.Context
  • database/sql
  • io.Reader
  • io.Writer
  • fs.FS
  • error

A package that accepts and returns standard interfaces is easier to introduce, test, and remove.

3. Measure removal cost before adoption

Ask:

How many application files change if this package is removed?

A parser may touch one adapter.

A framework may touch every handler.

4. Separate mechanism from policy

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.

5. Prefer explicit generated code over runtime magic

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.

6. One owner per concern

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.

7. Evaluate maintenance state, not commit frequency alone

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.

8. Review the dependency tree

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.

9. Check licenses and module identity

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)

10. Pin tools and automate updates

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.

11. Prefer boring infrastructure and custom domain policy

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.

12. Do not migrate working dependencies without a concrete payoff

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.


Final recommendation

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.

@garyblankenship

garyblankenship commented Jun 17, 2025

Copy link
Copy Markdown
Author

Awesome Go Packages: 2026 Index

This index tracks the current recommendations in the article.

HTTP and APIs

  • Chi - Composable routing on net/http
  • Huma - Implementation-first OpenAPI 3.1 APIs
  • oapi-codegen - Specification-first OpenAPI code generation
  • coder/websocket - Default WebSocket review choice

PostgreSQL and SQL

  • pgx - PostgreSQL driver and connection pool
  • sqlc - Type-safe Go generated from stable SQL
  • Scany - Flexible row scanning
  • goqu - Dynamic SQL construction

SQLite and Migrations

Configuration and HTTP Clients

  • caarlos0/env - Environment-variable decoding
  • Koanf - Multi-source configuration
  • Resty - Higher-level HTTP client
  • backoff - Bounded retry timing

CLI and Terminal UI

  • Kong - Struct-driven CLI parsing
  • Cobra - Large command ecosystems
  • Huh - Interactive terminal forms
  • Bubble Tea - Stateful terminal applications

Logging, Observability, and Caching

Concurrency and Testing

  • errgroup - Coordinated goroutine lifecycles
  • conc - Structured concurrency helpers and pools
  • go-cmp - Semantic test comparison
  • Testify - Concise assertions
  • testscript - CLI integration tests
  • goleak - Goroutine leak detection

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment