Integración con bases de datos (ej., PostgreSQL)

Lectura
25 min~5 min lectura

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:

  1. Instala el driver de PostgreSQL para Go usando go get github.com/lib/pq.
  2. Configura una estructura de conexión con parámetros como host, puerto, usuario, contraseña y nombre de la base de datos.
  3. Inicializa un pool de conexiones usando sql.Open y configuración adicional para optimizar el rendimiento (ej., máximo de conexiones, tiempo de vida).
  4. En tus handlers de gorilla/mux, usa el pool para ejecutar consultas SQL (SELECT, INSERT, UPDATE, DELETE) y manejar transacciones cuando sea necesario.
  5. 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 SetMaxOpenConns y SetMaxIdleConns según la carga esperada.
  • Ignorar el manejo de errores en consultas: No verificar sql.ErrNoRows o errores de conexión puede causar respuestas incorrectas o pánicos. Solución: Usa sentencias if err != nil y maneja casos específicos.
  • Olvidar cerrar recursos: No cerrar Rows o Transactions despues de usarlos genera fugas de memoria. Solución: Usa defer rows.Close() o defer tx.Rollback().
  • No usar contextos para timeouts: Consultas sin límite de tiempo pueden bloquear el servidor indefinidamente. Solución: Implementa QueryRowContext con 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:

  1. Configura una base de datos PostgreSQL local o en un contenedor Docker con una tabla products que tenga columnas: id (SERIAL PRIMARY KEY), name (VARCHAR), price (DECIMAL).
  2. Crea un proyecto Go e instala las dependencias: gorilla/mux y lib/pq.
  3. Implementa una función initDB que inicialice un pool de conexiones con configuración para máximo 20 conexiones abiertas y 5 inactivas.
  4. 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.
  5. Asegúrate de usar contextos con timeout de 2 segundos en todas las consultas.
  6. Prueba el microservicio con herramientas como curl o Postman, verificando que las operaciones CRUD funcionen correctamente.
Pistas
  • 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 con defer.
  • 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.