sqlconn

sqlconn manages a database/sql pool across a service lifetime: pool limits, connectivity verification, health checks, and idempotent shutdown.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/sqlconn"

Opens a database/sql pool, applies limits, verifies it works, exposes a health check, and closes it on shutdown.

c, err := sqlconn.Connect(ctx, "mysql://user:pass@tcp(host:3306)/db")
defer c.Shutdown()

New takes driver and DSN separately; Connect takes a <DRIVER>://<DSN> string, with WithDefaultDriver for a bare DSN.

The context bounds connection, not the pool

This is the distinction the package documents most carefully, and the one that causes the most confusion.

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

That is correct, and the alternative would be worse. sql.Open does not connect, so a service that never verified connectivity discovers a bad DSN or an unreachable host on its first query, in a request. Verifying at startup with a bounded context means a misconfigured deployment fails at rollout.

Closing the pool is wired separately:

  • WithLifetimeContext for a long-lived context tied to the process
  • WithShutdownSignalChan for a signal channel
  • SQLConn.Shutdown called directly

Shutdown closes the handle, updates the shared shutdown waitgroup, sets the internal DB pointer to nil to prevent further use, and stops the watcher goroutine. It is idempotent, so a deferred call and a signal-driven one can both fire without a double-close panic or a leaked goroutine.

Pool limits, and why the defaults are wrong

WithConnMaxOpen, WithConnMaxIdleCount, WithConnMaxIdleTime and WithConnMaxLifetime map to the sql.DB setters and are applied on successful connect.

Go’s defaults are unsuited to a service. MaxOpenConns defaults to unlimited, so a traffic spike opens connections until the database refuses them, converting a latency problem into an outage across every client. MaxIdleConns defaults to 2, so a busy service closes and reopens connections continuously, paying the handshake, and with TLS that cost is significant.

ConnMaxLifetime matters wherever something between the service and the database closes idle connections: a load balancer, a proxy, or a MySQL wait_timeout. Without it the pool hands out a connection the far end already closed, and the first query on it fails. Setting a lifetime shorter than the far end’s timeout means the pool retires connections before anything else does.

Health checks

SQLConn satisfies the healthcheck HealthChecker contract directly, so it registers with no adapter.

The check uses the same pool as real traffic, so it exercises the path production uses rather than a separate connection that might succeed while the pool is exhausted.

Injection points for tests

WithConnectFunc, WithCheckConnectionFunc and WithSQLOpenFunc replace the connect, verify and open steps.

These make the failure paths testable without a database: connect failure, verification failure, and a pool that opens but does not work are all reachable in a unit test.

Dependencies

Reaches the SQL driver you register. The package itself uses database/sql from the standard library.