Integração com Banco de Dados
A maioria das aplicações Gin do mundo real precisa de um banco de dados. Este guia cobre como estruturar o acesso ao banco de dados de forma limpa, configurar pool de conexões e aplicar padrões que mantêm seus handlers testáveis e suas conexões saudáveis.
Usando database/sql com Gin
Seção intitulada “Usando database/sql com Gin”O pacote database/sql da biblioteca padrão do Go funciona bem com o Gin. Abra a conexão uma vez no main e passe-a para seus 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")}Sempre passe c.Request.Context() para chamadas de banco de dados para que queries sejam canceladas automaticamente quando o cliente desconectar.
Configuração do pool de conexões
Seção intitulada “Configuração do pool de conexões”O pacote database/sql mantém um pool de conexões internamente. Para cargas de trabalho de produção, configure o pool para corresponder ao seu servidor de banco de dados e perfil de tráfego.
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)SetMaxOpenConnsprevine que sua aplicação sobrecarregue o servidor de banco de dados sob alta carga.SetMaxIdleConnsmantém conexões prontas para que novas requisições evitem o custo de criar novas conexões.SetConnMaxLifetimerotaciona conexões para que sua aplicação acompanhe mudanças de DNS e não mantenha sessões do lado do servidor obsoletas.SetConnMaxIdleTimefecha conexões que ficaram ociosas por muito tempo, liberando recursos em ambos os lados.
Padrões de injeção de dependência
Seção intitulada “Padrões de injeção de dependência”Closure
Seção intitulada “Closure”A abordagem mais simples é usar closure sobre o *sql.DB nas suas funções handler.
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
Seção intitulada “Middleware”Armazene a conexão no contexto do Gin para que qualquer handler possa recuperá-la.
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 com métodos
Seção intitulada “Struct com métodos”Agrupe handlers relacionados em uma struct que contém o handle do banco de dados. Esta abordagem escala bem quando você tem muitos handlers que compartilham as mesmas dependências.
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")}Os padrões de closure e struct são geralmente preferidos em relação ao middleware porque fornecem segurança de tipos em tempo de compilação e evitam type assertions em tempo de execução.
Usando GORM com Gin
Seção intitulada “Usando GORM com Gin”GORM é um ORM popular para Go. Ele envolve database/sql e adiciona migrations, associações e um construtor de queries.
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")}Tratamento de transações em handlers de requisição
Seção intitulada “Tratamento de transações em handlers de requisição”Quando uma requisição precisa realizar múltiplas escritas que devem ter sucesso ou falhar juntas, use uma transação de banco de dados.
Com database/sql
Seção intitulada “Com 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"})})Com GORM
Seção intitulada “Com 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"})})O método Transaction do GORM cuida do Begin, Commit e Rollback automaticamente com base no erro retornado.
Melhores práticas
Seção intitulada “Melhores práticas”- Inicialize a conexão de banco de dados no
maine compartilhe-a via closures, uma struct ou middleware. Nunca abra uma nova conexão por requisição. - Sempre use queries parametrizadas. Passe entrada do usuário como argumentos (
$1,?) em vez de concatenar strings. Isso previne injeção SQL. - Configure o pool de conexões para produção. Defina
MaxOpenConns,MaxIdleConnseConnMaxLifetimepara valores que correspondam aos limites do seu servidor de banco de dados e tráfego esperado. - Trate erros de conexão graciosamente. Chame
db.Ping()na inicialização para falhar rapidamente. Nos handlers, retorne códigos de status HTTP significativos e evite vazar detalhes internos de erro para os clientes. - Passe o contexto da requisição para queries. Use
c.Request.Context()para que queries de longa duração sejam canceladas quando o cliente desconectar ou um timeout disparar. - Feche
*sql.Rowscomdefer. Não fechar rows vaza conexões de volta ao pool.