# Go service

HTTP APIs and backend services with a straightforward binary deployment.

net/http · pgx · sqlc · PostgreSQL

[Complete guide (Markdown)](/stacks/go-service/guide.md) · [Structured record (JSON)](/stacks/go-service/record.json)

Original source review: 2026-10-10. Core guidance rechecked: 2026-10-13. [Check scope](/research-freshness.json). Dependency combinations and setup need project-specific qualification.

## Use cases

- APIs

- control planes

- concurrent network services

- background workers

## Choose it when

- Prefer explicit service code and standard tooling

- Kubernetes or Go infrastructure integration matters

- PostgreSQL already owns business state

## Consider another path when

- Need a batteries-included product framework

- Hard real-time or no-GC constraints

- Rich client UI is the primary product

## Working practices

- Propagate contexts and set timeouts on external requests. Bound concurrency and handle shutdown.

- Keep SQL and migrations reviewed and generated query code synchronized with the schema.

- Test handlers with httptest and persistence against a disposable real PostgreSQL instance.

- Use River only when durable jobs are required; test retry-safe effects and worker shutdown.

## Tools by job

### HTTP server: net/http

Standard handlers and request contexts minimize routing infrastructure.

Set request/body limits, deadlines and graceful shutdown; middleware and authorization remain application work.

- [https://pkg.go.dev/net/http](https://pkg.go.dev/net/http)

### Optional server-rendered UI: templ + HTMX

Compiled components and HTML-fragment updates suit forms and dashboards.

Generation adds a build step; advanced client state still needs browser-side design and testing.

- [https://templ.guide/](https://templ.guide/)

- [https://htmx.org/docs/](https://htmx.org/docs/)

### Database and migrations: PostgreSQL + pgx + sqlc + goose

Visible SQL, generated typed access and one migration owner.

Generators do not prove query efficiency; review transaction boundaries, pool budgets and online migration behavior. Never mix migration owners.

- [https://github.com/jackc/pgx](https://github.com/jackc/pgx)

- [https://docs.sqlc.dev/en/latest/](https://docs.sqlc.dev/en/latest/)

- [https://github.com/pressly/goose](https://github.com/pressly/goose)

- [https://github.com/golang-migrate/migrate](https://github.com/golang-migrate/migrate)

### Configuration: Typed settings loaded with os.LookupEnv

Explicit startup validation and standard structured logs.

Define precedence; reject malformed required values; redact secrets. Environment access alone is not validation or secret management.

- [https://pkg.go.dev/os](https://pkg.go.dev/os)

- [https://pkg.go.dev/flag](https://pkg.go.dev/flag)

- [https://pkg.go.dev/log/slog](https://pkg.go.dev/log/slog)

### Application logging: log/slog

Explicit startup validation and standard structured logs.

Define precedence; reject malformed required values; redact secrets. Environment access alone is not validation or secret management.

- [https://pkg.go.dev/os](https://pkg.go.dev/os)

- [https://pkg.go.dev/flag](https://pkg.go.dev/flag)

- [https://pkg.go.dev/log/slog](https://pkg.go.dev/log/slog)

### Tests and static checks: testing; go vet; race detector; govulncheck

Standard toolchain feedback plus exercised concurrency and known-vulnerability checks.

Race detector requires supported target/toolchain and exercised code; no scanner proves absence of vulnerabilities.

- [https://pkg.go.dev/testing](https://pkg.go.dev/testing)

- [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)

- [https://golangci-lint.run/docs/welcome/quick-start/](https://golangci-lint.run/docs/welcome/quick-start/)

- [https://go.dev/doc/security/vuln/](https://go.dev/doc/security/vuln/)

### HTTP and database integration tests: httptest + real disposable PostgreSQL

Exercise protocols and actual SQL/migrations.

Database tests need an isolated database; mocks cannot establish PostgreSQL semantics.

- [https://pkg.go.dev/net/http/httptest](https://pkg.go.dev/net/http/httptest)

- [https://github.com/jackc/pgx](https://github.com/jackc/pgx)

### Durable work: River for PostgreSQL-backed jobs

Transactional enqueueing ties work to committed business data.

External effects need idempotency. Default resumable checkpoints may be lost on crash; use transactional checkpoints where required. Inspect Pro feature boundaries.

- [https://riverqueue.com/docs/transactional-enqueueing](https://riverqueue.com/docs/transactional-enqueueing)

- [https://riverqueue.com/docs/resumable-jobs](https://riverqueue.com/docs/resumable-jobs)

- [https://docs.temporal.io/develop/go](https://docs.temporal.io/develop/go)

### Deployment and distribution: Go build binary in a supervised process or container; GoReleaser for releases

Small explicit runtime boundary; release automation builds target artifacts.

Target OS/architecture, libc/cgo, CA certificates, timezone data and signing need explicit verification. Snapshot builds are not publication.

- [https://goreleaser.com/getting-started/quick-start/](https://goreleaser.com/getting-started/quick-start/)

### Browser identity: go-oidc + existing OIDC provider

Provider discovery and ID-token verification support owned login sessions

Use v3 module; configure issuer/client, OAuth callback state and session lifecycle. go-oidc verifies ID tokens; API access-token validation follows the provider contract. Authorization stays near data.

- [https://github.com/coreos/go-oidc](https://github.com/coreos/go-oidc)

### Transactional email: go-mail + project-selected SMTP provider

Typed mail construction and SMTP transport support application messages

Configure TLS, sender credentials and timeouts; send required mail through durable jobs with duplicate protection. Validate chosen async/runtime features in Rust; delivery receipts and bounces need provider handling.

- [https://pkg.go.dev/github.com/wneessen/go-mail](https://pkg.go.dev/github.com/wneessen/go-mail)

### Uploads and object storage: AWS SDK for Go S3 client

Durable object storage avoids dependence on service-local disk

Configure runtime credentials, region and least-privilege bucket access; authorize object keys and uploads, bound size/concurrency and define retention. Test multipart cleanup and selected provider compatibility.

- [https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/go_s3_code_examples.html](https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/go_s3_code_examples.html)

### Realtime transport: coder/websocket

WebSocket support fits bidirectional connected clients

Enable the documented module/feature; authorize handshake and every protected message, bound queues/message size and implement heartbeat/shutdown. Socket delivery is not durable history; define reconnect/resync.

- [https://pkg.go.dev/github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket)

Build with the stack’s strengths

## Architecture and developer workflow.

Recommendation: a Go HTTP process owns request admission and cancellation, domain packages own invariants, and a bounded SQL connection pool provides parallel database access. Durable jobs have their own persisted queue and worker lifecycle. Goroutines and buffered channels are concurrency mechanisms; graceful drain does not turn them into a recoverable queue.

Version scope: Go 1.27 release documentation and current net/http/context APIs; sqlc latest transaction guide with pgx/v5; River current documentation, PostgreSQL composition. Resolve and record Go toolchain, sqlc, pgx and River versions in go.mod/go.sum and tool configuration before implementation. Official release notes identify Go 1.27 as the August 2026 release; this research does not qualify a lockfile or patch-version/security support. · Reviewed 2026-10-11

### Architecture boundaries

- Recommendation: compose net/http handlers around small domain packages and explicit interfaces at external boundaries. Pass request contexts into SQL and outbound calls. Use bounded errgroup fan-out or a worker pool, with an overload policy separate from concurrency limits.

- Recommendation: maintain reviewed SQL migrations and sqlc-generated queries; share a pool, acquire transactions only around domain writes, and call generated WithTx for the transaction. A single global mutex around all queries recreates avoidable serialization.

- Recommendation: PostgreSQL-backed River workers can run with the web binary initially or in an independent process when their scaling or shutdown needs differ. Insert business state and a job in the same database transaction when atomic enqueue is required; cross-database work needs a separately designed outbox.

- Recommendation: retain an established identity provider, object store, email sender and broker during a service rewrite. Request/response, cookies, WebSocket application protocol, job payload and retry semantics are separate compatibility contracts.

### Tools and feedback

- Recommendation: start from the existing repository; inspect go.mod, go.sum, schema migrations and queue configuration. For a new local source project, go mod init example.com/service creates a module; choose the actual module path and supported toolchain deliberately.

- Recommendation: pin sqlc using the maintained tool installer, review sqlc.yaml and SQL source, then run sqlc generate after query/schema changes. Regenerate in CI and compare output; generated typing does not validate production schema drift.

- Recommendation: run gofmt on changed files, go vet ./..., go test ./... and go test -race ./... on a supported target. Add HTTP contract fixtures and cancellation/queue crash tests against the selected database; these are proposed checks, not executed here.

- Recommendation: build the service with go build ./cmd/server when that entry point exists. Set server/header/body deadlines, worker limits, pool capacity and shutdown budget from the deployment shape. Profile a representative workload with Go pprof; performance and cost remain unmeasured.

### Translations to avoid

- Avoid one fire-and-forget goroutine per email/payment: an acknowledged request can survive while its side effect disappears on process crash.

- Avoid spawning unlimited goroutines to hide slow SQL. The downstream pool becomes the queue; cancellation, memory and overload must be bounded explicitly.

- Avoid replacing Redis solely because Go has channels. Shared fan-out, sessions, queue persistence and locks have different semantics.

- Avoid treating Server.Shutdown as complete WebSocket shutdown; hijacked connections need separate notification and tracking.

[go.dev →](https://go.dev/doc/go1.27)
[go.dev →](https://go.dev/doc/modules/managing-dependencies)
[docs.sqlc.dev →](https://docs.sqlc.dev/en/latest/howto/transactions.html)
[go.dev →](https://go.dev/doc/database/manage-connections)
[riverqueue.com →](https://riverqueue.com/docs/transactional-enqueueing)
[riverqueue.com →](https://riverqueue.com/docs/job-retries)
[riverqueue.com →](https://riverqueue.com/docs/ruby)
[pkg.go.dev →](https://pkg.go.dev/net/http#Server.Shutdown)
[pkg.go.dev →](https://pkg.go.dev/golang.org/x/sync/errgroup)
[pkg.go.dev →](https://pkg.go.dev/os/signal#NotifyContext)
[github.com →](https://github.com/coder/websocket)
[redis.io →](https://redis.io/docs/latest/develop/pubsub/)

For an existing application

## Keep the contracts while you change the stack.

These are documented options and proposed checks. Inspect the actual source application before adopting a transition.

Data and schema

### Inspect the source

Inventory the old schema, isolation, identifiers, null handling, timestamps and transaction invariants before changing language or driver.

### During coexistence

Recommendation: let the old application own migrations while Go accesses backward-compatible columns through sqlc; route writes for one domain/cohort to one owner. Expand schema before either runtime needs it.

### Target design

Recommendation: sqlc plus pgx/v5 or database/sql, a shared bounded pool and explicit transaction-scoped queries. Keep the existing database if its guarantees fit; PostgreSQL replacement is a separate migration.

### Rollback

Recommendation: return domain traffic to the old writer while preserving expanded columns. Contract migrations wait until the rollback window closes; reconcile any new-only writes first.

### Benefits and prerequisites

**Typed query access and concurrent pool usage can improve maintainability and remove an unnecessary application-wide lock.**
Requires: Queries generated from the reviewed schema; representative null/time/transaction fixtures; a pool sized across all replicas.
Tradeoffs: Generated types do not prove isolation or business correctness; pool exhaustion and migrations still need operations.

### Checks before transfer

- Verify old-write/new-read and new-write/old-read round trips, including nulls and time zones.

- Force a mid-transaction failure; neither half of the domain change may commit.

- Saturate the pool and cancel a request; verify bounded waiting and connection release.

[docs.sqlc.dev →](https://docs.sqlc.dev/en/latest/howto/transactions.html)
[go.dev →](https://go.dev/doc/database/manage-connections)

Jobs and schedules

### Inspect the source

Capture existing queue kind, JSON arguments, retry/schedule/dead-letter states and external-effect idempotency. Redis/Resque or Sidekiq payloads are not River payloads.

### During coexistence

Recommendation: keep old workers consuming old queues. Route selected job kinds into River through versioned producers; transform outstanding states explicitly and retain one scheduler. River Ruby integration is a possible bridge, not automatic Active Job execution.

### Target design

Recommendation: River PostgreSQL jobs with domain writes plus InsertTx in the same transaction, explicit queue concurrency and bounded retry policy. Keep Redis for any remaining broker/session/cache responsibility.

### Rollback

Recommendation: stop new River producers and reroute only unstarted work after reconciliation. Keep a compatible River consumer for already accepted jobs; a broker switch cannot undo sent messages or payments.

### Benefits and prerequisites

**Atomic enqueue closes the gap between committing domain state and creating work.**
Requires: Domain rows and River insertion use the same transaction/database; validated job-kind/argument mapping; replay-safe external effects.
Tradeoffs: Adds queue schema/maintenance to the database; retries can repeat effects; backlog migration and adapter compatibility are separate work.

### Checks before transfer

- Abort the domain transaction; no job may become available.

- Crash after sending an email but before job completion; retry must reconcile or reuse a stable external idempotency key.

- Account for ready, scheduled, retry, failed and in-flight jobs before retiring the old consumer.

[riverqueue.com →](https://riverqueue.com/docs/transactional-enqueueing)
[riverqueue.com →](https://riverqueue.com/docs/job-retries)
[riverqueue.com →](https://riverqueue.com/docs/ruby)

Delivery and recovery

### Inspect the source

Preserve response/error behavior, overload rules, deadlines and restart guarantees; establish separate budgets for HTTP and background jobs.

### During coexistence

Recommendation: serve a fixed tenant/endpoint cohort from Go behind existing routing, with errgroup/context cancellation inside each operation and deployment-level readiness. Keep the old instance ready for contract-compatible rollback.

### Target design

Recommendation: signal.NotifyContext, bounded admission/fan-out, Server.Shutdown with a fresh timeout context, tracked worker termination and pool closure after accepted work ends.

### Rollback

Recommendation: return routing to the old process and inspect committed outcomes and queued work before any replay. Preserve identical database/schema contracts for both versions.

### Benefits and prerequisites

**Cooperative cancellation limits work that callers no longer need and makes planned shutdown observable.**
Requires: Every called operation honors context; in-flight tasks are tracked; the platform shutdown grace period exceeds the configured drain budget.
Tradeoffs: Cancellation does not reverse a committed write or external effect; forced termination still loses memory-only work.

### Checks before transfer

- Cancel a client while SQL/outbound I/O is blocked; verify resources and goroutines settle.

- Send SIGTERM during a write; the server stops admitting traffic and records whether accepted work committed.

- Force SIGKILL; verify durable jobs recover independently of graceful drain.

[pkg.go.dev →](https://pkg.go.dev/net/http#Server.Shutdown)
[pkg.go.dev →](https://pkg.go.dev/golang.org/x/sync/errgroup)
[pkg.go.dev →](https://pkg.go.dev/os/signal#NotifyContext)

Realtime transport

### Inspect the source

Preserve authentication, subscription identifiers, frame formats, ordering, reconnect cursors and cross-runtime fan-out. A WebSocket transport does not implement Action Cable automatically.

### During coexistence

Recommendation: keep the existing broker and explicit application-protocol adapter while moving complete room/tenant cohorts, including reconnects, to Go. Publish/subscribe in both directions using captured protocol fixtures.

### Target design

Recommendation: coder/websocket for transport plus bounded per-connection queues, tracked close handshakes and an application resynchronization path. Retain Redis Pub/Sub if ephemeral cross-process fan-out fits; add durable history separately where missed events matter.

### Rollback

Recommendation: route the cohort and reconnects back together. Keep the broker and old protocol alive; rebuild client state from durable records rather than pretending ephemeral events can be replayed.

### Benefits and prerequisites

**Explicit connection ownership and queue limits bound slow-client resource use.**
Requires: Capacity and disconnect policy are defined; clients can reconnect and reconstruct authoritative state; broker/protocol compatibility is tested.
Tradeoffs: Redis Pub/Sub is at-most-once and does not replay disconnected messages; replacing it with process-local channels removes cross-instance delivery.

### Checks before transfer

- Connect an old and new client to the same room; verify broadcasts in both directions and revocation.

- Disconnect a subscriber and publish; reconnect must fetch missed authoritative state.

- Throttle one client; verify the configured queue overflow policy and independent delivery to others.

[github.com →](https://github.com/coder/websocket)
[redis.io →](https://redis.io/docs/latest/develop/pubsub/)
[pkg.go.dev →](https://pkg.go.dev/net/http#Server.Shutdown)

[Read the migration workflow →](/rewrite/guide.md)

## Setup

- Start in a new source checkout in a local development workspace; run go mod init example.com/project using the real module path.

- Resolve and record Go/tool versions and dependencies; go get github.com/jackc/pgx/v5; add River only if jobs are needed.

- Create cmd/server plus application packages, typed settings, explicit http.Server limits, health/readiness and shutdown handling.

- Install selected sqlc and goose releases; write migrations and sqlc configuration; run sqlc generate. Configure a disposable PostgreSQL database through environment/secrets.

- With GOOSE_DRIVER=postgres and GOOSE_DBSTRING targeting only the disposable database, run goose -dir migrations up. Add business transaction and River enqueue tests.

- Add the chosen release configuration, pin tools and preserve go.mod/go.sum; document build and rollback. These steps have not been run.

## Verification

Run these in the configured application; they are instructions, not recorded passes.

```
gofmt -l .
go test ./...
go test -race ./...
go vet ./...
govulncheck ./...
sqlc generate
goose -dir migrations status
go build ./cmd/server
```

## Open questions

- Which API contracts, browser login or service-token requirements apply?

- Does realtime need one-way updates or bidirectional sockets?

- Which database pool, worker throughput, storage and deployment/restore budgets apply?

## Sources

- [Go net/http](https://pkg.go.dev/net/http)

- [templ](https://templ.guide/)

- [HTMX](https://htmx.org/docs/)

- [pgx PostgreSQL driver](https://github.com/jackc/pgx)

- [sqlc documentation](https://docs.sqlc.dev/en/latest/)

- [goose migrations](https://github.com/pressly/goose)

- [golang-migrate](https://github.com/golang-migrate/migrate)

- [Go os configuration primitives](https://pkg.go.dev/os)

- [Go flag](https://pkg.go.dev/flag)

- [Go structured logging](https://pkg.go.dev/log/slog)

- [Go testing](https://pkg.go.dev/testing)

- [Go data race detector](https://go.dev/doc/articles/race_detector)

- [golangci-lint quick start](https://golangci-lint.run/docs/welcome/quick-start/)

- [Go vulnerability management](https://go.dev/doc/security/vuln/)

- [Go HTTP testing](https://pkg.go.dev/net/http/httptest)

- [River transactional enqueueing](https://riverqueue.com/docs/transactional-enqueueing)

- [River resumable jobs](https://riverqueue.com/docs/resumable-jobs)

- [Temporal Go SDK](https://docs.temporal.io/develop/go)

- [GoReleaser quick start](https://goreleaser.com/getting-started/quick-start/)

- [go-oidc repository](https://github.com/coreos/go-oidc)

- [go-mail documentation](https://pkg.go.dev/github.com/wneessen/go-mail)

- [AWS Go S3 examples](https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/go_s3_code_examples.html)

- [coder/websocket documentation](https://pkg.go.dev/github.com/coder/websocket)

---

Canonical page: https://stack.menu/stacks/go-service/
