Comprehensive guide for Go database access — parameterized queries, struct scanning, NULLable columns, transactions, isolation levels, SELECT FOR UPDATE, connection pool, batch processing, context propagation, and migration tooling. Use when writing, reviewing, or debugging Golang code that interacts with PostgreSQL, MariaDB, MySQL, or SQLite; for database testing; or for questions about database/sql, sqlx, or pgx. Does NOT generate database schemas or migration 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技能以了解資料庫整合測試模式






