Go 資料庫存取的完整指南 — 參數化查詢、結構掃描、可為 NULL 的欄位、交易、隔離層級、SELECT FOR UPDATE、連線池、批次處理、Context 傳遞以及遷移工具。適用於撰寫、審查或除錯與 PostgreSQL、MariaDB、MySQL 或 SQLite 互動的 Golang 程式碼;用於資料庫測試;或關於 database/sql、sqlx 或 pgx 的問題。不產生資料庫綱要或遷移 SQL。
角色: 你是一位 Go 後端工程師,撰寫安全、明確且可觀察的資料庫程式碼。你將 SQL 視為第一級語言 — 沒有 ORM,沒有魔法 — 並且在邊界處捕獲資料完整性問題,而不是在應用程式深處。
模式:
- 撰寫模式 — 產生新的儲存庫函式、查詢輔助工具或交易包裝器:遵循技能的順序指示;在背景啟動代理程式,在產生新程式碼前搜尋程式碼庫中現有的查詢模式和命名慣例。
- 審查/除錯模式 — 審計或除錯現有的資料庫程式碼:使用子代理程式掃描遺失的
rows.Close()、未參數化的查詢、遺失的 Context 傳遞以及遺漏的錯誤檢查,同時並行讀取業務邏輯。
社群預設。 明確取代
samber/cc-skills-golang@golang-database技能的團隊技能具有優先權。
Go 資料庫最佳實務
Go 的 database/sql 為資料庫存取提供了堅實的基礎。在其上使用 sqlx 或 pgx 以獲得更好的操作體驗 — 永遠不要使用 ORM。
使用 sqlx 或 pgx 時,請參考該函式庫的官方文件和程式碼範例以取得最新的 API 簽名。
最佳實務摘要
- 使用 sqlx 或 pgx,不要用 ORM — ORM 隱藏 SQL,產生不可預測的查詢,並使除錯更困難
- 查詢必須使用參數化佔位符 — 絕不要將使用者輸入串接到 SQL 字串中
- Context 必須傳遞給所有資料庫操作 — 使用
*Context方法變體(QueryContext、ExecContext、GetContext) sql.ErrNoRows必須明確處理 — 使用errors.Is區分「找不到」與真正的錯誤- Rows 必須在迭代後關閉 — 在
QueryContext呼叫後立即使用defer rows.Close() - 絕不要對不傳回行的陳述使用
db.Query—Query傳回*Rows,必須關閉;如果忘記,連線會洩漏回池中。請使用db.Exec - 對多陳述操作使用交易 — 將相關寫入包裝在
BeginTxx/Commit中 - 在讀取打算修改的資料時使用
SELECT ... FOR UPDATE— 防止競爭條件 - 設定自訂隔離層級 — 當預設的 READ COMMITTED 不足時(例如,金融操作使用可序列化)
- 使用指標欄位(
*string、*int)或sql.NullXxx型別處理可為 NULL 的欄位 - 必須設定連線池 —
SetMaxOpenConns、SetMaxIdleConns、SetConnMaxLifetime、SetConnMaxIdleTime - 使用外部工具進行遷移 — golang-migrate 或 Flyway,絕不要用手寫或 AI 產生的遷移 SQL
- 以合理的大小進行批次操作 — 不要逐行(太多往返),也不要一次數百萬行(鎖定和記憶體)
- 絕不要建立或修改資料庫綱要 — 在玩具資料上看起來正確的綱要可能在真實生產負載下產生熱點、鎖定競爭或遺失索引。綱要設計需要理解資料量、存取模式和生產限制,而 AI 並不具備這些
- 避免隱藏的 SQL 功能 — 不要在應用程式程式碼中依賴觸發器、檢視、具體化檢視、預存程序或列級安全性
函式庫選擇
| 函式庫 | 最適合 | 結構掃描 | PostgreSQL 專用 |
|---|---|---|---|
database/sql |
可攜性、最小依賴 | 手動 Scan |
否 |
sqlx |
多資料庫專案 | StructScan |
否 |
pgx |
PostgreSQL(快 30-50%) | pgx.RowToStructByName |
是(COPY、LISTEN、陣列) |
| GORM/ent | 避免 | 魔法 | 被抽象化 |
為什麼不使用 ORM:
- 不可預測的查詢產生 — 在程式碼中看不到的 N+1 問題
- 魔法鉤子和回呼(BeforeCreate、AfterUpdate)使除錯更困難
- 綱要遷移與應用程式程式碼耦合
- 學習 ORM API 比學習 SQL 更難,而且抽象會洩漏
參數化查詢
// ✗ 非常糟糕 — SQL 注入漏洞
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
// ✓ 良好 — 參數化(PostgreSQL)
var user User
err := db.GetContext(ctx, &user, "SELECT id, name, email FROM users WHERE email = $1", email)
// ✓ 良好 — 參數化(MySQL)
err := db.GetContext(ctx, &user, "SELECT id, name, email FROM users WHERE email = ?", email)
動態 IN 子句
query, args, err := sqlx.In("SELECT * FROM users WHERE id IN (?)", ids)
if err != nil {
return fmt.Errorf("building IN clause: %w", err)
}
query = db.Rebind(query) // 根據驅動程式調整佔位符
err = db.SelectContext(ctx, &users, query, args...)
動態欄位名稱
絕不要從使用者輸入中內插欄位名稱。使用允許清單:
allowed := map[string]bool{"name": true, "email": true, "created_at": true}
if !allowed[sortCol] {
return fmt.Errorf("invalid sort column: %s", sortCol)
}
query := fmt.Sprintf("SELECT id, name, email FROM users ORDER BY %s", sortCol)
更多注入防護模式,請參閱 samber/cc-skills-golang@golang-security 技能。
結構掃描與可為 NULL 的欄位
對 sqlx 使用 db:"column_name" 標籤,對 pgx 使用 pgx.CollectRows 搭配 pgx.RowToStructByName。使用指標欄位(*string、*time.Time)處理可為 NULL 的欄位 — 它們在掃描和 JSON 序列化時都能正常運作。請參閱掃描參考以取得所有方法的範例。
錯誤處理
func GetUser(id string) (*User, error) {
var user User
err := db.GetContext(ctx, &user, "SELECT id, name FROM users WHERE id = $1", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound // 轉換為領域錯誤
}
return nil, fmt.Errorf("querying user %s: %w", id, err)
}
return &user, nil
}
或者:
func GetUser(id string) (u *User, exists bool, err error) {
var user User
err := db.GetContext(ctx, &user, "SELECT id, name FROM users WHERE id = $1", id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, false, nil // 「沒有使用者」不是技術錯誤,而是領域錯誤
}
return nil, false, fmt.Errorf("querying user %s: %w", id, err)
}
return &user, true, nil
}
永遠關閉 rows
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return fmt.Errorf("querying users: %w", err)
}
defer rows.Close() // 防止連線洩漏
for rows.Next() {
// ...
}
if err := rows.Err(); err != nil { // 迭代後務必檢查
return fmt.Errorf("iterating users: %w", err)
}
常見資料庫錯誤模式
| 錯誤 | 如何偵測 | 動作 |
|---|---|---|
| 找不到資料列 | errors.Is(err, sql.ErrNoRows) |
回傳領域錯誤 |
| 唯一性約束 | 檢查驅動程式特定的錯誤碼 | 回傳衝突錯誤 |
| 連線被拒絕 | db.PingContext 回傳 err != nil |
快速失敗、記錄、以退避重試 |
| 序列化失敗 | PostgreSQL 錯誤碼 40001 |
重試整個交易 |
| Context 已取消 | errors.Is(err, context.Canceled) |
停止處理、傳播 |
Context 傳遞
始終使用 *Context 方法變體來傳遞截止時間和取消:
// ✗ 糟糕 — 沒有 context,即使客戶端中斷連線,查詢也會執行到完成
db.Query("SELECT ...")
// ✓ 良好 — 尊重 context 取消和逾時
db.QueryContext(ctx, "SELECT ...")
有關 context 模式的深入探討,請參閱 samber/cc-skills-golang@golang-context 技能。
交易、隔離層級與鎖定
有關交易模式、隔離層級、SELECT FOR UPDATE 和鎖定變體,請參閱交易。
連線池
db.SetMaxOpenConns(25) // 限制總連線數
db.SetMaxIdleConns(10) // 保持熱連線就緒
db.SetConnMaxLifetime(5 * time.Minute) // 回收過時連線
db.SetConnMaxIdleTime(1 * time.Minute) // 更快關閉閒置連線
有關大小調整指南和公式,請參閱資料庫效能。
遷移
使用外部遷移工具。綱要變更需要人類審查,並理解資料量、現有索引、外鍵和生產限制。
推薦工具:
- golang-migrate — CLI + Go 函式庫,支援所有主要資料庫
- Flyway — 基於 JVM,廣泛用於企業環境
- Atlas — 現代、宣告式綱要管理
遷移 SQL 應由人類撰寫和審查,在原始碼控制中進行版本管理,並透過 CI/CD 管道套用。
避免隱藏的 SQL 功能
不要在應用程式程式碼中依賴觸發器、檢視、具體化檢視、預存程序或列級安全性 — 它們會產生不可見的副作用,使除錯變得不可能。將 SQL 保持明確且可見於 Go 中,以便進行測試和版本控制。
綱要建立
此技能不涵蓋綱要建立。 AI 產生的綱要通常有微妙的錯誤 — 遺失索引、不正確的欄位型別、錯誤的正規化或遺失約束。綱要設計需要理解資料量、存取模式、查詢輪廓和業務限制。請使用專用的資料庫工具和人工審查。
深入探討
- 交易 — 交易邊界、隔離層級、死結預防、
SELECT FOR UPDATE - 測試資料庫程式碼 — Mock 連線、使用容器的整合測試、測試資料、綱要設定/拆除
- 資料庫效能 — 連線池大小調整、批次處理、索引策略、查詢最佳化
- 結構掃描 — 結構標籤、可為 NULL 的欄位處理、JSON 序列化模式
交叉參考
- → 請參閱
samber/cc-skills-golang@golang-security技能以了解 SQL 注入防護模式 - → 請參閱
samber/cc-skills-golang@golang-context技能以了解資料庫操作的 context 傳遞 - → 請參閱
samber/cc-skills-golang@golang-error-handling技能以了解資料庫錯誤包裝模式 - → 請參閱
samber/cc-skills-golang@golang-testing技能以了解資料庫整合測試模式






