database/sql & net Package Family Overview
Go splits networking and SQL into layered packages that share context and io patterns.
database/sql manages connection pools and transactions.
The net family (net, net/http, net/url, net/netip) handles addresses, HTTP, and modern IP types.
Summary
Import a SQL driver anonymously, open a *sql.DB, and configure pool limits for your database SLA.
HTTP servers and clients built on net/http reuse the same timeout and context practices as SQL queries.
Use net/netip for typed IPs and prefixes instead of parsing strings ad hoc.
Recipe
Quick-reference recipe card - copy-paste ready.
import (
"context"
"database/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"net/http"
"time"
)
db, _ := sql.Open("pgx", dsn)
db.SetMaxOpenConns(25)
db.SetConnMaxLifetime(30 * time.Minute)
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
row := db.QueryRowContext(ctx, `SELECT name FROM users WHERE id=$1`, id)
})When to reach for this:
- Building REST or RPC services that read/write PostgreSQL, MySQL, or SQLite.
- Writing TCP clients, DNS lookups, or custom protocols with
net.Conn. - Sharing deadline and cancellation across HTTP handlers and database calls.
- Parsing URLs and client IPs consistently at the edge.
Working Example
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "file:users.db?cache=shared&mode=rwc")
if err != nil {
panic(err)
}
defer db.Close()
db.SetMaxOpenConns(1) // sqlite single-writer
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)`); err != nil {
panic(err)
}
if _, err := db.Exec(`INSERT OR IGNORE INTO users (id, name) VALUES (1, 'Ada')`); err != nil {
panic(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
handleUser(db, w, r)
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
fmt.Println("listening", srv.Addr)
panic(srv.ListenAndServe())
}
func handleUser(db *sql.DB, w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "bad remote addr", http.StatusInternalServerError)
return
}
var name string
err = db.QueryRowContext(ctx, `SELECT name FROM users WHERE id=1`).Scan(&name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"name": name,
"peer": host,
})
}What this demonstrates:
sql.Openwith driver import and pool tuning.QueryRowContexthonoring request deadlines.net.SplitHostPortfor peer metadata.- Production-oriented
http.ServerwithReadHeaderTimeout.
Deep Dive
How It Works
sql.Opencreates a pool handle; first real connection opens on first query.- Drivers implement
database/sql/driverinterfaces; pgx, mysql, and sqlite ship as modules. net.Dialerandnet.Listenmanage TCP/UDP sockets;http.Serverlayers HTTP parsing above.contextcancellation closes in-flight queries and HTTP handler work when deadlines hit.
Package Family Map
| Package | Responsibility | Typical pairing |
|---|---|---|
database/sql | Pool, Tx, Stmt | Driver module (pgx, go-sql-driver/mysql) |
net | IPs, dial, listen | Custom protocols, gRPC over TCP |
net/http | HTTP server/client | REST gateways, webhooks |
net/url | URL parse/build | Redirects, query strings |
net/netip | Typed Addr, Prefix | ACL checks, CIDR routing |
Go Notes
// Always ping after Open to catch bad DSN early:
if err := db.PingContext(ctx); err != nil { return err }
// Prepared statements for hot queries:
stmt, err := db.PrepareContext(ctx, `SELECT ...`)
// Prefer netip over net.ParseIP for comparisons:
addr, _ := netip.ParseAddr("203.0.113.1")Gotchas
- Calling
sql.OpenwithoutPing- Misconfigured DSN fails on first request. Fix:PingContextduring startup health checks. - Unbounded
SetMaxOpenConns- Exhausts database connections under spike load. Fix: set max open and idle conns from DB capacity formulas. - Using
http.DefaultClientin servers - No timeouts on outbound calls. Fix: dedicatedhttp.Clientper upstream withTransporttuning. - Ignoring
rows.Err()after iteration - Silent partial results. Fix: checkerrafterfor rows.Next()loop. - Storing
*sql.Txon structs - Leaks transactions across requests. Fix: keep transactions scoped to handler or service method. - Parsing IPs with string splits - IPv6 and zone IDs break naive code. Fix:
net/netip.ParseAddrornet.SplitHostPort.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| ORM (gorm, ent) | Complex models and migrations | Simple SQL with few tables |
pgx pool directly | PostgreSQL-only advanced features | Need database/sql portability |
gRPC (google.golang.org/grpc) | Internal RPC contracts | Browser-facing JSON APIs |
database/sql + sqlc | Type-safe query codegen | Ad-hoc dynamic SQL only |
FAQs
Why is the SQL driver a blank import?
Drivers register themselves with database/sql in init.
The blank import links the driver without referencing its package directly.
What is the difference between sql.DB and sql.Tx?
sql.DB is a connection pool shared across goroutines.
sql.Tx binds one transactional session; do not share across requests.
How do HTTP timeouts affect database calls?
Pass r.Context() into QueryContext.
When the server read/write deadline fires, context cancels and the driver should stop the query.
When should I use net/netip?
Whenever comparing, parsing, or storing IP addresses and CIDR prefixes.
It avoids mutability and parsing bugs in net.IP slice form.
Does database/sql support connection retries?
Not automatically.
Implement retry at the application layer for transient errors, with backoff and idempotency guards.
How does net/http relate to net.Conn?
HTTP servers accept connections via net.Listener, then parse HTTP on top.
Custom protocols can use net.Dial and net.Conn without HTTP.
What DSN format does sqlite use here?
The example uses modernc.org/sqlite with a file: URI.
Driver-specific DSN strings differ; consult driver docs for PostgreSQL and MySQL.
Should I use Query or QueryContext?
Always prefer QueryContext in servers so cancellation propagates.
Bare Query uses context.Background() internally.
How do I share one http.Client across handlers?
Inject a configured client from main into handlers or a service struct.
Tune Transport.MaxIdleConnsPerHost per upstream dependency.
Can I use database/sql with multiple drivers?
Each sql.Open call uses one driver name.
Different databases mean different *sql.DB handles, usually different services.
What is net/url for in APIs?
Parse request URLs, build redirect targets, and encode query parameters without manual string concat.
How do I health-check SQL in Kubernetes?
Expose /health that runs db.PingContext with a short timeout.
Fail readiness when the pool cannot reach the primary database.
Related
- The Go Standard Library: Batteries Included - Stdlib layering model
- time, timezones & context Integration - Deadlines through the stack
- net/http: Go's Batteries-Included HTTP Stack - HTTP deep dive
- crypto/*, compress & math/rand/v2 - TLS for listeners
- Standard Library Best Practices - Pool and client defaults
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).