本文 AI 產出,尚未審核
Golang 資料庫操作:使用 database/sql 連接 SQL 資料庫
簡介
在開發後端服務或資料驅動的應用程式時,資料庫的存取是不可或缺的核心功能。Go 語言內建的 database/sql 標準套件提供了一套抽象層,讓開發者可以以統一的方式操作 MySQL、PostgreSQL、SQLite 等多種關聯式資料庫,而不必為每種資料庫寫不同的程式碼。
本篇文章將從 概念、實作範例、常見陷阱與最佳實踐 四個面向,帶領讀者快速上手 database/sql,並說明在真實專案中如何安全、有效率地使用它。
核心概念
1. sql.DB 與連線池
sql.DB 不是單一的資料庫連線,而是一個 連線池 (connection pool)。它會自動管理底層的 TCP 連線,根據需求建立或關閉連線,並提供以下重要屬性:
| 屬性 | 說明 |
|---|---|
MaxOpenConns |
同時允許的最大開啟連線數。預設 0 代表無限制。 |
MaxIdleConns |
允許保持閒置的連線數量。預設 2。 |
ConnMaxLifetime |
單一連線的最長存活時間,避免因為資料庫端的 timeout 而發生錯誤。 |
Tip:在應用程式啟動時就建立
*sql.DB,並在整個生命週期內重複使用,切勿在每次查詢時重新sql.Open。
2. 驅動程式 (Driver)
database/sql 本身只提供介面,實際與資料庫通訊的工作交由 驅動程式 完成。常見的驅動有:
- MySQL:
github.com/go-sql-driver/mysql - PostgreSQL:
github.com/lib/pq(或pgx) - SQLite:
github.com/mattn/go-sqlite3
使用時只需要在 import 時加上 _ 以執行 side‑effect 註冊:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // MySQL driver
)
3. 執行查詢與指令
Query:返回多筆資料,回傳*sql.Rows。QueryRow:只返回單筆資料,回傳*sql.Row。Exec:執行 INSERT、UPDATE、DELETE 等不回傳結果集的指令,回傳sql.Result。
4. 參數化查詢 (Prepared Statement)
使用 ?(MySQL)或 $1(PostgreSQL)等佔位符可以避免 SQL Injection,同時讓資料庫快取執行計畫,提高效能。
stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES (?, ?)")
程式碼範例
以下示範 5 個常見且實用的操作,每段程式碼皆附有說明註解。
範例 1:建立資料庫連線與設定連線池
package main
import (
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN 格式: username:password@protocol(address)/dbname?param=value
dsn := "root:secret@tcp(127.0.0.1:3306)/testdb?parseTime=true"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Open DB error: %v", err)
}
// 確保在程式結束前關閉連線池
defer db.Close()
// 設定連線池參數
db.SetMaxOpenConns(25) // 最多 25 個同時連線
db.SetMaxIdleConns(5) // 最多 5 個閒置連線
db.SetConnMaxLifetime(5 * time.Minute) // 5 分鐘後自動關閉
// Ping 確認連線可用
if err = db.Ping(); err != nil {
log.Fatalf("Ping DB error: %v", err)
}
log.Println("Database connection established")
}
範例 2:使用 QueryRow 取得單筆資料
type User struct {
ID int
Name string
Age int
}
func GetUserByID(db *sql.DB, id int) (*User, error) {
var u User
// 使用參數化查詢避免 SQL Injection
err := db.QueryRow("SELECT id, name, age FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Name, &u.Age)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // 找不到資料,回傳 nil
}
return nil, err
}
return &u, nil
}
範例 3:遍歷多筆結果 (Query)
func ListUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, age FROM users ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close() // 確保資源釋放
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Age); err != nil {
return nil, err
}
users = append(users, u)
}
// 檢查迭代過程中的錯誤
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
}
範例 4:使用 Exec 實作 INSERT、UPDATE、DELETE
// 新增使用者
func CreateUser(db *sql.DB, name string, age int) (int64, error) {
result, err := db.Exec("INSERT INTO users(name, age) VALUES (?, ?)", name, age)
if err != nil {
return 0, err
}
// 取得自動產生的主鍵 (若有)
return result.LastInsertId()
}
// 更新使用者年齡
func UpdateUserAge(db *sql.DB, id int, newAge int) (int64, error) {
result, err := db.Exec("UPDATE users SET age = ? WHERE id = ?", newAge, id)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
// 刪除使用者
func DeleteUser(db *sql.DB, id int) (int64, error) {
result, err := db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
範例 5:使用事務 (Transaction) 保證資料一致性
func TransferPoints(db *sql.DB, fromID, toID, points int) error {
tx, err := db.Begin() // 開始事務
if err != nil {
return err
}
// 確保事務結束時正確提交或回滾
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // 重新拋出 panic
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()
// 從來源扣點
_, err = tx.Exec("UPDATE users SET points = points - ? WHERE id = ?", points, fromID)
if err != nil {
return err
}
// 加到目標
_, err = tx.Exec("UPDATE users SET points = points + ? WHERE id = ?", points, toID)
return err
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳做法 |
|---|---|---|
忘記 defer rows.Close() |
會導致底層連線不釋放,最終耗盡連線池。 | 每次取得 *sql.Rows 後立即 defer rows.Close()。 |
直接使用 sql.Open 產生新連線 |
sql.Open 只建立連線池,若在迴圈內重複呼叫會浪費資源。 |
在程式啟動階段建立一次 *sql.DB,全域或依賴注入使用。 |
| 未使用參數化查詢 | 容易遭受 SQL Injection。 | 永遠使用 ? / $1 佔位符,或 Prepare 後重複執行。 |
忽略 RowsAffected 或 LastInsertId |
無法判斷操作是否成功或取得自增鍵。 | 使用 sql.Result 取得影響列數或新鍵值。 |
| 事務未正確提交或回滾 | 當錯誤發生時資料會處於不一致狀態。 | 使用 defer 包裹 Commit/Rollback,如上範例所示。 |
錯誤處理只檢查 err != nil |
某些情況(如 sql.ErrNoRows)需要特別處理。 |
針對 sql.ErrNoRows 做出適當回應,避免把「查無資料」當成系統錯誤。 |
實際應用場景
Web API 後端
- 使用
database/sql與 Gin、Echo 等框架結合,完成 CRUD API。 - 透過連線池與事務保證高併發下的效能與資料一致性。
- 使用
批次資料匯入/匯出
- 以
Tx包裹大量INSERT,配合Stmt(prepared statement)提升寫入速度。 - 使用
Rows逐筆讀取大表,避免一次載入全部記憶體。
- 以
微服務間的共享資料
- 每個微服務只負責自己的資料表,透過
database/sql抽象層保持程式碼一致性,未來若換成 PostgreSQL 只需要更換 driver 即可。
- 每個微服務只負責自己的資料表,透過
監控與告警
- 利用
db.Stats()取得連線池統計資訊(如OpenConnections、InUse),配合 Prometheus 監控資料庫健康狀態。
- 利用
總結
database/sql為 Go 提供了 統一且安全的資料庫存取介面,配合各式驅動即可支援主流的關聯式資料庫。- 了解
*sql.DB連線池、參數化查詢、事務 等核心概念,才能寫出 效能佳、避免資源泄漏 的程式。 - 實務上,一次建立
*sql.DB、全域重用、正確處理Rows、Result、ErrNoRows,以及 使用defer包裹事務,是最常見的最佳實踐。
掌握以上技巧後,你就能在 Go 專案中自信地操作 SQL 資料庫,無論是簡單的 CRUD API,還是高併發的金融交易系統,都能保持 安全、可靠且具可維護性。祝開發順利!