Data and Messaging

SQL connection lifecycle and transactions, distributed locking, Redis, Valkey, Kafka, S3, and SQS clients in nurago

The stateful dependencies a backend service talks to. Every client here exposes a HealthCheck(ctx) error method that satisfies the healthcheck interface directly, so registering a dependency in the readiness endpoint needs no adapter.

SQL Connections

sqlconn manages a database/sql pool through the lifetime of a long-running service: applying pool limits, verifying connectivity, exposing a health check, and closing on shutdown. It reaches no external module (you supply the driver).

c, err := sqlconn.Connect(
    ctx,
    "mysql://user:pass@tcp(localhost:3306)/appdb",
    sqlconn.WithDefaultDriver("mysql"),
    sqlconn.WithShutdownSignalChan(shutdownCh),
    sqlconn.WithShutdownWaitGroup(&shutdownWG),
)

Connect accepts a <DRIVER>://<DSN> string; New takes the driver and DSN separately.

The context passed to New or Connect bounds connection establishment only: dialling and the initial health check. It does not own the pool. A request-scoped or timeout-scoped context will not close the pool when it ends.

To close the pool at shutdown, wire a long-lived context with WithLifetimeContext, a shutdown channel with WithShutdownSignalChan, or call Shutdown directly. Shutdown is idempotent and stops the watcher goroutine, so a deferred call and the signal path can both fire safely.

Transactions

sqltransaction runs a function inside a transaction and owns the control flow: begin, run, commit on success, roll back on error or panic.

err := sqltransaction.Exec(ctx, db, func(ctx context.Context, tx *sql.Tx) error {
    // all related statements on tx; return an error to roll back
    return nil
})

The rollback path is guarded so it does not manufacture noise. It is skipped after a successful commit, and sql.ErrTxDone during rollback is ignored. A rollback failure is joined onto the current error instead of replacing it, so both halves of the diagnosis survive.

sqlxtransaction is the same helper for sqlx, when you want struct scanning inside the transaction. ExecWithOptions on either package sets the isolation level and read-only flag.

sqlutil quotes identifiers and string literals for query fragments that must be assembled dynamically. Read the boundary it states before using it on anything untrusted: see /docs/security/.

Distributed Locking

mysqllock provides process-distributed mutual exclusion on MySQL’s GET_LOCK and RELEASE_LOCK. Acquire takes a key and returns a ReleaseFunc that must be called. It reaches no external module beyond the driver you already have.

It suits the case where a job must run on one replica at a time and MySQL is already a dependency, so no new infrastructure is introduced. A session-scoped lock has real limits, which the package documentation states precisely: read them before relying on the lock for correctness.

Redis and Valkey

redis wraps go-redis and valkey wraps valkey-go for Valkey. The two present nearly the same API, so moving between the stores is largely a change of import and options type.

Both give you raw key/value access (Set, Get, Del, with expiration), typed access that encodes and decodes Go values through pluggable codec hooks (SetData, GetData), Pub/Sub in raw and typed form (Send, Receive, SendData, ReceiveData), a HealthCheck that sends a PING, and a missing key surfacing as ErrKeyNotFound. Every error state is an exported sentinel matchable with errors.Is.

The server address is validated before any connection is attempted. Subscriptions declared with WithChannels start a background subscription that runs until Close: cancelling the context passed to New does not stop it, so Close is required, not optional, when channels are configured.

For redis, WithChannelOptions tunes the subscription buffer, send timeout, and health check interval. With the go-redis defaults, a consumer that stops calling Receive loses messages once the 100-message buffer has stayed full for one minute. Set those values explicitly where the consumer can stall.

Kafka

kafka is a pure-Go producer and consumer built on kafka-go: no CGO, no system librdkafka, so it cross-compiles like the rest of your service.

The delivery semantics are explicit, and the package asks you to choose between them. With a consumer group configured:

  • Receive and ReceiveData are at-most-once: the offset is committed as soon as the message is read, before your code processes it. A crash or a decode failure after the read skips the message permanently.
  • FetchMessage plus CommitMessages is at-least-once: the offset is committed only when you acknowledge, after successful processing.

With no consumer group (empty group ID), offsets are never committed: both read paths behave identically, reading always starts from the earliest available offset, and CommitMessages returns an error.

Producer writes wait for acknowledgment from the full in-sync replica set by default (kafka.RequireAll); WithRequiredAcks relaxes that. Typed payloads run through the same encode and decode hooks as the other clients, defaulting to encode.

AWS

awsopt is the shared configuration layer: a composable slice of config.LoadOptionsFunc values, built once and handed to any AWS-based package here, materialized into an aws.Config by LoadDefaultConfig. WithRegionFromURL derives the region from a service URL so it does not have to be repeated.

s3 is a bucket-scoped client for the common object operations: Put from an io.Reader, Get with a body stream, ListKeys and ListObjects by prefix (the latter with size, last-modified, and ETag), Delete, and a HealthCheck that verifies both reachability and access permissions. WithEndpointMutable and WithEndpointImmutable point it at a local S3-compatible environment for tests.

sqs covers the queue workflow: send, receive, decode, acknowledge, health-check. Argument validation runs before any AWS configuration is loaded, and the FIFO rules are enforced rather than left to the API: a .fifo queue URL requires a message group ID, a standard queue rejects one. Send and SendData set no deduplication ID, so a FIFO queue needs content-based deduplication enabled, or use SendWithDeduplicationID. Long polling defaults to 20 seconds and visibility to 600.

The receive path shapes the calling code in two places. Receive returns nil, nil when the long-poll window expires with no message. When decoding fails, ReceiveData still returns the receipt handle, leaving the choice between deleting the poison message and letting it re-queue to you.

awssecretcache caches Secrets Manager lookups locally with single-flight deduplication, bounded in size.

Encoding Across Boundaries

encode serializes and deserializes values crossing system boundaries: databases, queues, caches, RPC payloads. It backs the default codecs of the Redis, Valkey, Kafka, and SQS clients, and replacing those codecs (WithMessageEncodeFunc, WithMessageDecodeFunc) is where you would add a custom wire format, encryption through encrypt, compression, or schema validation.


Previous: /docs/security/

Overview: /docs/

Next: /docs/testing/