資料庫整合
大多數實際的 Gin 應用程式都需要資料庫。本指南介紹如何乾淨地組織資料庫存取、配置連線池,以及應用讓處理函式可測試且連線健康的模式。
在 Gin 中使用 database/sql
Go 標準函式庫的 database/sql 套件與 Gin 配合良好。在 main 中開啟一次連線,並將其傳遞給處理函式。
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")}務必將 c.Request.Context() 傳遞給資料庫呼叫,這樣當客戶端斷線時查詢會自動取消。
連線池配置
database/sql 套件在內部維護一個連線池。對於正式環境工作負載,請配置連線池以匹配你的資料庫和流量特性。
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)SetMaxOpenConns防止你的應用程式在高負載下壓垮資料庫伺服器。SetMaxIdleConns保持暖連線就緒,這樣新請求就不需要建立連線的成本。SetConnMaxLifetime輪替連線,讓你的應用程式能取得 DNS 變更,且不會持有過時的伺服器端 Session。SetConnMaxIdleTime關閉閒置過久的連線,釋放雙方的資源。
依賴注入模式
閉包
最簡單的方式是在處理函式中閉包 *sql.DB。
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")}中介軟體
將連線儲存在 Gin 上下文中,這樣任何處理函式都可以取得它。
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")}帶方法的結構體
將相關的處理函式分組到一個持有資料庫控制代碼的結構體中。當你有許多共享相同依賴項的處理函式時,此方式擴展良好。
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")}閉包和結構體模式通常優於中介軟體,因為它們提供編譯期型別安全性並避免執行期的型別斷言。
在 Gin 中使用 GORM
GORM 是流行的 Go ORM。它包裝了 database/sql 並新增了遷移、關聯和查詢建構器。
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")}在請求處理函式中處理交易
當請求需要執行多個必須一起成功或失敗的寫入操作時,使用資料庫交易。
使用 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"})})使用 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"})})GORM 的 Transaction 方法會根據回傳的錯誤自動處理 Begin、Commit 和 Rollback。
最佳實務
- 在
main中初始化資料庫連線,並透過閉包、結構體或中介軟體共享。切勿為每個請求開啟新連線。 - 務必使用參數化查詢。 將使用者輸入作為參數(
$1、?)傳遞,而非串接字串。這可以防止 SQL 注入。 - 為正式環境配置連線池。 將
MaxOpenConns、MaxIdleConns和ConnMaxLifetime設為與你的資料庫伺服器限制和預期流量匹配的值。 - 優雅地處理連線錯誤。 在啟動時呼叫
db.Ping()以快速失敗。在處理函式中,回傳有意義的 HTTP 狀態碼,避免向客戶端洩露內部錯誤細節。 - 將請求上下文傳遞給查詢。 使用
c.Request.Context(),這樣當客戶端斷線或逾時觸發時,長時間執行的查詢會被取消。 - 使用
defer關閉*sql.Rows。 未關閉 rows 會導致連線洩露回連線池。