本文 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 後重複執行。
忽略 RowsAffectedLastInsertId 無法判斷操作是否成功或取得自增鍵。 使用 sql.Result 取得影響列數或新鍵值。
事務未正確提交或回滾 當錯誤發生時資料會處於不一致狀態。 使用 defer 包裹 Commit/Rollback,如上範例所示。
錯誤處理只檢查 err != nil 某些情況(如 sql.ErrNoRows)需要特別處理。 針對 sql.ErrNoRows 做出適當回應,避免把「查無資料」當成系統錯誤。

實際應用場景

  1. Web API 後端

    • 使用 database/sql 與 Gin、Echo 等框架結合,完成 CRUD API。
    • 透過連線池與事務保證高併發下的效能與資料一致性。
  2. 批次資料匯入/匯出

    • Tx 包裹大量 INSERT,配合 Stmt(prepared statement)提升寫入速度。
    • 使用 Rows 逐筆讀取大表,避免一次載入全部記憶體。
  3. 微服務間的共享資料

    • 每個微服務只負責自己的資料表,透過 database/sql 抽象層保持程式碼一致性,未來若換成 PostgreSQL 只需要更換 driver 即可。
  4. 監控與告警

    • 利用 db.Stats() 取得連線池統計資訊(如 OpenConnectionsInUse),配合 Prometheus 監控資料庫健康狀態。

總結

  • database/sql 為 Go 提供了 統一且安全的資料庫存取介面,配合各式驅動即可支援主流的關聯式資料庫。
  • 了解 *sql.DB 連線池參數化查詢事務 等核心概念,才能寫出 效能佳、避免資源泄漏 的程式。
  • 實務上,一次建立 *sql.DB、全域重用正確處理 RowsResultErrNoRows,以及 使用 defer 包裹事務,是最常見的最佳實踐。

掌握以上技巧後,你就能在 Go 專案中自信地操作 SQL 資料庫,無論是簡單的 CRUD API,還是高併發的金融交易系統,都能保持 安全、可靠且具可維護性。祝開發順利!