Integración con base de datos
La mayoría de las aplicaciones Gin del mundo real necesitan una base de datos. Esta guía cubre cómo estructurar el acceso a la base de datos de forma limpia, configurar el pool de conexiones y aplicar patrones que mantengan tus handlers testeables y tus conexiones saludables.
Usando database/sql con Gin
El paquete database/sql de la biblioteca estándar de Go funciona bien con Gin. Abre la conexión una vez en main y pásala a tus handlers.
package main
import ( "database/sql" "log" "net/http"
"github.com/gin-gonic/gin" _ "github.com/lib/pq")
func main() { db, err := sql.Open("postgres", "host=localhost port=5432 user=app dbname=mydb sslmode=disable") if err != nil { log.Fatal(err) } defer db.Close()
// Verify the connection is alive if err := db.Ping(); err != nil { log.Fatal(err) }
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) { var name string err := db.QueryRowContext(c.Request.Context(), "SELECT name FROM users WHERE id = $1", c.Param("id")).Scan(&name) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) return } c.JSON(http.StatusOK, gin.H{"name": name}) })
r.Run(":8080")}Siempre pasa c.Request.Context() a las llamadas de base de datos para que las consultas se cancelen automáticamente cuando el cliente se desconecta.
Configuración del pool de conexiones
El paquete database/sql mantiene un pool de conexiones internamente. Para cargas de trabajo en producción, configura el pool para que coincida con tu base de datos y perfil de tráfico.
db, err := sql.Open("postgres", dsn)if err != nil { log.Fatal(err)}
// Maximum number of open connections to the database.db.SetMaxOpenConns(25)
// Maximum number of idle connections retained in the pool.db.SetMaxIdleConns(10)
// Maximum amount of time a connection may be reused.db.SetConnMaxLifetime(5 * time.Minute)
// Maximum amount of time a connection may sit idle before being closed.db.SetConnMaxIdleTime(1 * time.Minute)SetMaxOpenConnspreviene que tu aplicación abrume al servidor de base de datos bajo alta carga.SetMaxIdleConnsmantiene conexiones preparadas para que las nuevas solicitudes eviten el costo de establecer conexión.SetConnMaxLifetimerota las conexiones para que tu app detecte cambios DNS y no mantenga sesiones obsoletas del lado del servidor.SetConnMaxIdleTimecierra conexiones que han estado inactivas demasiado tiempo, liberando recursos en ambos lados.
Patrones de inyección de dependencias
Closure
El enfoque más simple es envolver *sql.DB en tus funciones handler mediante closures.
package main
import ( "database/sql" "net/http"
"github.com/gin-gonic/gin")
func listUsers(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { rows, err := db.QueryContext(c.Request.Context(), "SELECT id, name FROM users") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } defer rows.Close()
type User struct { ID int `json:"id"` Name string `json:"name"` } var users []User for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Name); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "scan failed"}) return } users = append(users, u) } c.JSON(http.StatusOK, users) }}
func main() { db, _ := sql.Open("postgres", "your-dsn") defer db.Close()
r := gin.Default() r.GET("/users", listUsers(db)) r.Run(":8080")}Middleware
Almacena la conexión en el contexto de Gin para que cualquier handler pueda recuperarla.
func DatabaseMiddleware(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { c.Set("db", db) c.Next() }}
func main() { db, _ := sql.Open("postgres", "your-dsn") defer db.Close()
r := gin.Default() r.Use(DatabaseMiddleware(db))
r.GET("/users/:id", func(c *gin.Context) { db := c.MustGet("db").(*sql.DB) // use db... _ = db })
r.Run(":8080")}Struct con métodos
Agrupa handlers relacionados en un struct que contiene el manejador de base de datos. Este enfoque escala bien cuando tienes muchos handlers que comparten las mismas dependencias.
type UserHandler struct { DB *sql.DB}
func (h *UserHandler) Get(c *gin.Context) { var name string err := h.DB.QueryRowContext(c.Request.Context(), "SELECT name FROM users WHERE id = $1", c.Param("id")).Scan(&name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } c.JSON(http.StatusOK, gin.H{"name": name})}
func main() { db, _ := sql.Open("postgres", "your-dsn") defer db.Close()
uh := &UserHandler{DB: db}
r := gin.Default() r.GET("/users/:id", uh.Get) r.Run(":8080")}Los patrones de closure y struct generalmente se prefieren sobre middleware porque proporcionan seguridad de tipos en tiempo de compilación y evitan aserciones de tipo en tiempo de ejecución.
Usando GORM con Gin
GORM es un ORM popular de Go. Envuelve database/sql y agrega migraciones, asociaciones y un constructor de consultas.
package main
import ( "net/http"
"github.com/gin-gonic/gin" "gorm.io/driver/postgres" "gorm.io/gorm")
type Product struct { gorm.Model Name string `json:"name"` Price float64 `json:"price"`}
func main() { dsn := "host=localhost user=app dbname=mydb port=5432 sslmode=disable" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { panic("failed to connect to database") }
// Auto-migrate the schema db.AutoMigrate(&Product{})
r := gin.Default()
r.POST("/products", func(c *gin.Context) { var p Product if err := c.ShouldBindJSON(&p); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := db.Create(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create product"}) return } c.JSON(http.StatusCreated, p) })
r.GET("/products/:id", func(c *gin.Context) { var p Product if err := db.First(&p, c.Param("id")).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "product not found"}) return } c.JSON(http.StatusOK, p) })
r.Run(":8080")}Manejo de transacciones en handlers de solicitud
Cuando una solicitud necesita realizar múltiples escrituras que deben tener éxito o fallar juntas, usa una transacción de base de datos.
Con database/sql
r.POST("/transfer", func(c *gin.Context) { tx, err := db.BeginTx(c.Request.Context(), nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "could not begin transaction"}) return } // Ensure rollback runs if we return early due to an error. defer tx.Rollback()
_, err = tx.ExecContext(c.Request.Context(), "UPDATE accounts SET balance = balance - $1 WHERE id = $2", 100, 1) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "debit failed"}) return }
_, err = tx.ExecContext(c.Request.Context(), "UPDATE accounts SET balance = balance + $1 WHERE id = $2", 100, 2) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "credit failed"}) return }
if err := tx.Commit(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "commit failed"}) return }
c.JSON(http.StatusOK, gin.H{"status": "transfer complete"})})Con GORM
r.POST("/transfer", func(c *gin.Context) { err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&Account{}).Where("id = ?", 1). Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil { return err } if err := tx.Model(&Account{}).Where("id = ?", 2). Update("balance", gorm.Expr("balance + ?", 100)).Error; err != nil { return err } return nil })
if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "transfer failed"}) return } c.JSON(http.StatusOK, gin.H{"status": "transfer complete"})})El método Transaction de GORM maneja Begin, Commit y Rollback automáticamente basándose en el error retornado.
Mejores prácticas
- Inicializa la conexión a la base de datos en
mainy compártela mediante closures, un struct o middleware. Nunca abras una nueva conexión por solicitud. - Siempre usa consultas parametrizadas. Pasa la entrada del usuario como argumentos (
$1,?) en lugar de concatenar cadenas. Esto previene la inyección SQL. - Configura el pool de conexiones para producción. Establece
MaxOpenConns,MaxIdleConnsyConnMaxLifetimea valores que coincidan con los límites de tu servidor de base de datos y el tráfico esperado. - Maneja los errores de conexión de forma elegante. Llama a
db.Ping()al inicio para fallar rápidamente. En los handlers, devuelve códigos de estado HTTP significativos y evita filtrar detalles internos de error a los clientes. - Pasa el contexto de la solicitud a las consultas. Usa
c.Request.Context()para que las consultas de larga duración se cancelen cuando el cliente se desconecta o un tiempo de espera se activa. - Cierra
*sql.Rowscondefer. No cerrar las filas filtra conexiones de vuelta al pool.