Concepto clave
La integración con bases de datos es el puente entre tu microservicio escrito en Go y el almacenamiento persistente de datos. Imagina que tu API es un restaurante: los clientes (usuarios) piden platos (solicitudes HTTP), la cocina (tu lógica en Go) los prepara, pero necesita ingredientes almacenados en una despensa (la base de datos). Sin una conexión eficiente a esa despensa, el restaurante no puede funcionar de manera consistente.
En el contexto de microservicios con gorilla/mux, esta integración implica configurar un pool de conexiones a PostgreSQL que maneje múltiples solicitudes concurrentes sin crear una nueva conexión por cada una. Esto es crucial para APIs de alto rendimiento, donde cientos o miles de usuarios pueden acceder simultáneamente. Un pool actúa como un conjunto de conexiones reutilizables, similar a un equipo de repartidores que recogen y entregan paquetes sin necesidad de contratar uno nuevo para cada envío.
Cómo funciona en la práctica
Para integrar PostgreSQL en tu microservicio Go, sigue estos pasos:
- Instala el driver de PostgreSQL para Go usando
go get github.com/lib/pq. - Configura una estructura de conexión con parámetros como host, puerto, usuario, contraseña y nombre de la base de datos.
- Inicializa un pool de conexiones usando
sql.Openy configuración adicional para optimizar el rendimiento (ej., máximo de conexiones, tiempo de vida). - En tus handlers de gorilla/mux, usa el pool para ejecutar consultas SQL (SELECT, INSERT, UPDATE, DELETE) y manejar transacciones cuando sea necesario.
- Cierra el pool al finalizar la aplicación para liberar recursos.
Este enfoque asegura que tu microservicio pueda manejar carga alta sin saturar la base de datos, manteniendo respuestas rápidas y confiables.
Codigo en accion
Aquí tienes un ejemplo básico de configuración del pool de conexiones y un handler que consulta datos:
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
)
var db *sql.DB
func initDB() {
connStr := "host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable"
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
// Configurar el pool
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
// Verificar conexión
if err := db.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("Conectado a PostgreSQL")
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", id).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Usuario no encontrado", http.StatusNotFound)
} else {
http.Error(w, "Error interno", http.StatusInternalServerError)
}
return
}
fmt.Fprintf(w, "Usuario: %s", name)
}
func main() {
initDB()
defer db.Close()
r := mux.NewRouter()
r.HandleFunc("/users/{id}", getUserHandler).Methods("GET")
log.Fatal(http.ListenAndServe(":8080", r))
}Ahora, mejora este código refactorizando para usar un contexto y manejar timeouts, evitando que consultas lentas bloqueen el servidor:
func getUserHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
vars := mux.Vars(r)
id := vars["id"]
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Usuario no encontrado", http.StatusNotFound)
} else if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "Timeout en la consulta", http.StatusRequestTimeout)
} else {
http.Error(w, "Error interno", http.StatusInternalServerError)
}
return
}
fmt.Fprintf(w, "Usuario: %s", name)
}Errores comunes
- No usar un pool de conexiones: Crear una nueva conexión por cada solicitud agota los recursos y ralentiza la API. Solución: Configura
SetMaxOpenConnsySetMaxIdleConnssegún la carga esperada. - Ignorar el manejo de errores en consultas: No verificar
sql.ErrNoRowso errores de conexión puede causar respuestas incorrectas o pánicos. Solución: Usa sentenciasif err != nily maneja casos específicos. - Olvidar cerrar recursos: No cerrar
RowsoTransactionsdespues de usarlos genera fugas de memoria. Solución: Usadefer rows.Close()odefer tx.Rollback(). - No usar contextos para timeouts: Consultas sin límite de tiempo pueden bloquear el servidor indefinidamente. Solución: Implementa
QueryRowContextcon un contexto con timeout. - Exponer detalles de la base de datos en errores: Enviar mensajes de error SQL al cliente compromete la seguridad. Solución: Logea los errores internamente y responde con mensajes genéricos al cliente.
Checklist de dominio
- Configurar un pool de conexiones a PostgreSQL con parámetros optimizados para tu carga.
- Implementar handlers en gorilla/mux que ejecuten consultas SELECT, INSERT, UPDATE y DELETE.
- Manejar transacciones para operaciones que requieren atomicidad (ej., transferencias).
- Usar contextos para controlar timeouts y cancelaciones en consultas largas.
- Validar y sanitizar entradas de usuario para prevenir inyecciones SQL.
- Probar la integración con una base de datos real o un contenedor Docker.
- Monitorizar métricas como tiempo de respuesta y número de conexiones activas.
Crear un microservicio CRUD con PostgreSQL y gorilla/mux
En este ejercicio, construirás un microservicio básico que gestione una lista de productos, integrando PostgreSQL para persistencia. Sigue estos pasos:
- Configura una base de datos PostgreSQL local o en un contenedor Docker con una tabla
productsque tenga columnas:id(SERIAL PRIMARY KEY),name(VARCHAR),price(DECIMAL). - Crea un proyecto Go e instala las dependencias:
gorilla/muxylib/pq. - Implementa una función
initDBque inicialice un pool de conexiones con configuración para máximo 20 conexiones abiertas y 5 inactivas. - Define handlers para:
- GET /products: Listar todos los productos.
- GET /products/{id}: Obtener un producto por ID.
- POST /products: Crear un nuevo producto (usa JSON en el body).
- PUT /products/{id}: Actualizar un producto existente.
- DELETE /products/{id}: Eliminar un producto.
- Asegúrate de usar contextos con timeout de 2 segundos en todas las consultas.
- Prueba el microservicio con herramientas como curl o Postman, verificando que las operaciones CRUD funcionen correctamente.
- Usa
json.NewDecoder(r.Body).Decode(&product)para parsear JSON en el handler POST. - Para el handler GET de todos los productos, recuerda iterar sobre
rows.Next()y cerrar las filas condefer. - En el handler PUT, considera usar una transacción si necesitas actualizar múltiples campos atómicamente.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.