database/sql es el motor de abstracción de Go para bases de datos relacionales. No implementa el protocolo de comunicación de ningún motor; en su lugar, define un conjunto de interfaces y tipos que los drivers (como pgx para PostgreSQL o go-sqlite3 para SQLite) deben satisfacer mediante el registro con sql.Register. Esta arquitectura permite que tu lógica de negocio sea agnóstica al motor de base de datos, separando la intención de la consulta del protocolo de red subyacente.
Cuando invocas a sql.Open, lo que realmente sucede es una validación de los argumentos de configuración; la conexión física no se establece inmediatamente. Para verificar la conectividad real con el servidor, es imperativo llamar a db.Ping(). Un aspecto crítico para el rendimiento es entender que un objeto *sql.DB no representa una conexión única, sino un pool de conexiones concurrente y thread-safe. Si intentas crear un sql.DB por cada solicitud en una API, destruirás el rendimiento de la aplicación al forzar un handshake de red y un proceso de autenticación en cada request, además de agotar rápidamente los descriptores de archivos o puertos del sistema operativo. Para monitorear la salud de este pool en producción, el runtime nos expone db.Stats(), que nos permite observar cuántas conexiones están en uso, cuántas están inactivas o cuántas goroutines están bloqueadas esperando una conexión disponible.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"sync"
"time"
// El import con guion bajo es un "side-effect import".
// Registra el driver en sql.Register mediante su función init().
_ "github.com/mattn/go-sqlite3"
)
func main() {
// sql.Open no establece la conexión; solo valida el DSN (Data Source Name).
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Usamos un contexto con timeout para asegurar que el Ping no bloquee indefinidamente.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Ping() es el primer contacto real con el motor de base de datos.
if err := db.PingContext(ctx); err != nil {
log.Fatalf("No se pudo establecer la conexión: %v", err)
}
// Configuración del pool de conexiones: esencial para producción.
db.SetMaxOpenConns(10) // Límite de conexiones simultáneas al motor.
db.SetMaxIdleConns(5) // Mantener algunas conexiones abiertas para evitar el handshake.
db.SetConnMaxLifetime(time.Minute) // Evita el uso de conexiones demasiado antiguas (zombies).
// Simulamos una carga de trabajo concurrente para ver el pool en acción.
var wg sync.WaitGroup
for i := 1; i <= 30; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
ejecutarTarea(db, id)
}(i)
}
wg.Wait()
imprimirEstadisticas(db)
}
func ejecutarTarea(db *sql.DB, id int) {
// Al ejecutar Query o Exec, el pool toma una conexión disponible
// o bloquea la goroutine si el límite (MaxOpenConns) se alcanzó.
var resultado int
err := db.QueryRow("SELECT 1").Scan(&resultado)
if err != nil {
fmt.Printf("[Worker %d] Error: %v\n", id, err)
return
}
// Simulamos latencia de red o procesamiento de base de datos.
time.Sleep(100 * time.Millisecond)
if id%10 == 0 {
fmt.Printf("[Worker %d] Tarea completada con éxito\n", id)
}
}
func imprimirEstadisticas(db *sql.DB) {
// Stats() nos devuelve la radiografía actual del pool.
stats := db.Stats()
fmt.Printf("\n--- Reporte del Pool de Conexiones ---\n")
fmt.Printf("Conexiones en uso (InUse): %d\n", stats.InUse)
fmt.Printf("Conexiones inactivas (Idle): %d\n", stats.Idle)
fmt.Printf("Conexiones en espera (WaitCount): %d\n", stats.WaitCount)
fmt.Printf("Total de conexiones creadas: %d\n", stats.MaxOpenConnections)
}
En el ejemplo anterior, fíjate en cómo sql.Open con el driver sqlite3 permite trabajar con una base de datos en memoria sin configurar una red compleja, pero la semántica de gestión es la misma que con PostgreSQL. Cuando las 30 goroutines intentan ejecutar db.QueryRow, el pool actúa como un regulador: debido a que limitamos SetMaxOpenConns(10), el runtime de Go no abrirá 30 conexiones, sino que mantendrá 10 activas y pondrá a las otras 20 en espera, gestionando la concurrencia de forma transparente. La función imprimirEstadisticas utiliza db.Stats() para mostrar este comportamiento, donde WaitCount nos indica cuántas veces las goroutines tuvieron que esperar por una conexión libre, un indicador clave de que el pool es demasiado pequeño para la carga actual.
El uso de db.SetMaxIdleConns(5) es una decisión de optimización: si configuramos este valor en 0, cada conexión se cerraría inmediatamente después de terminar la consulta, obligando a realizar un nuevo handshake TCP/TLS para la siguiente operación. Al mantener conexiones inactivas, ganamos latencia a costa de un uso mínimo de recursos en el servidor de base de datos.
El error frecuente
Un error clásico es instanciar sql.Open dentro de un manejador de peticiones HTTP:
// MAL: Esto destruirá el rendimiento de tu servicio.
func handler(w http.ResponseWriter, r *http.Request) {
db, _ := sql.Open("postgres", "user=pq...")
defer db.Close()
// Cada request abre y cierra una conexión física.
db.QueryRow("SELECT ...")
}
Este patrón anula todos los beneficios del pooling. Cada petición HTTP incurre en el costo de establecer la conexión, lo que dispara la latencia y puede agotar rápidamente el número de sockets disponibles en el servidor (TIME_WAIT en el stack TCP), provocando errores de “connection refused” incluso si la base de datos está sana. La instancia de *sql.DB debe crearse una sola vez al inicio de la aplicación y compartirse mediante inyección de dependencias.
N° 195