database/sql
是 Go 語言標準庫中用于與 SQL(或類 SQL)數據庫交互的核心包,提供了一套輕量級、通用的接口,使得開發者可以用統一的方式操作各種不同的數據庫,而無需關心底層數據庫驅動的具體實現。
核心設計理念
database/sql
包本身并不包含任何特定數據庫的驅動程序,只是定義了一系列接口,而具體的數據庫驅動則需要實現這些接口。這種設計的好處在于:
- 通用性: 開發者可以編寫與特定數據庫無關的數據訪問代碼。
- 可移植性: 只需更換導入的數據庫驅動和連接字符串,即可輕松切換數據庫。
- 專注性:
database/sql
包專注于提供統一的數據庫操作抽象,而將底層細節交由第三方驅動實現。
在使用時,通常會像下面這樣導入 database/sql
包和一個匿名的數據庫驅動包:
import ("database/sql"_ "github.com/go-sql-driver/mysql" // 匿名導入,僅執行其init()函數
)
匿名導入(使用下劃線 _
)的目的是執行驅動包的 init()
函數,該函數會將驅動注冊到 database/sql
中。之后就可以通過 database/sql
提供的函數來使用這個驅動了。
核心類型
database/sql
包主要由以下幾個核心類型組成:
類型 | 描述 |
---|---|
sql.DB | 表示一個數據庫句柄,代表一個維護了零到多個底層連接的連接池。它是并發安全的,應該被視為一個長生命周期的對象,通常在應用啟動時創建,并在整個應用生命周期內共享。 |
sql.Tx | 表示一個數據庫事務。在事務中執行的所有操作要么全部成功(Commit ),要么全部失敗(Rollback )。 |
sql.Stmt | 表示一個預編譯的 SQL 語句。預編譯可以防止 SQL 注入,并且在重復執行相同語句時能提升性能。 |
sql.Rows | 表示一個查詢返回的多行結果集。使用 Next() 方法遍歷每一行,并用 Scan() 方法讀取行中的數據。 |
sql.Row | 表示一個查詢返回的單行結果。通常用于期望最多返回一行結果的查詢。 |
sql.Result | 表示 Exec 方法(用于 INSERT , UPDATE , DELETE 等操作)的執行結果,可以獲取最后插入的 ID 和受影響的行數。 |
sql.Null* 類型 | 如 sql.NullString , sql.NullInt64 , sql.NullBool 等,用于處理可能為 NULL 的數據庫列。每個類型都包含一個 Valid 字段(布爾型)來表示值是否為 NULL ,以及一個 Value 字段來存儲實際的值。 |
基本操作
1. 連接數據庫
使用 sql.Open()
函數來創建一個 sql.DB
對象。這個函數需要兩個參數:驅動名稱和數據源名稱(Data Source Name, DSN)。
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {log.Fatal(err)
}
defer db.Close()
注意: sql.Open()
并不會立即建立與數據庫的連接,也不會驗證連接參數的有效性,只是準備好數據庫抽象對象。要驗證連接是否有效,可以使用 db.Ping()
方法。
err = db.Ping()
if err != nil {log.Fatal("數據庫連接失敗: ", err)
}
sql.DB
對象內部管理著一個連接池,開發者無需手動管理連接。可以通過以下方法配置連接池:
db.SetMaxOpenConns(n)
: 設置連接池中的最大打開連接數。db.SetMaxIdleConns(n)
: 設置連接池中的最大空閑連接數。db.SetConnMaxLifetime(d)
: 設置連接可被復用的最大時間。
2. 執行查詢
查詢多行
使用 db.Query()
或 db.QueryContext()
方法執行返回多行結果的 SELECT
查詢。
rows, err := db.Query("SELECT id, name FROM users WHERE age > ?", 30)
if err != nil {log.Fatal(err)
}
defer rows.Close()type User struct {ID int64Name string
}var users []User
for rows.Next() {var u Userif err := rows.Scan(&u.ID, &u.Name); err != nil {log.Fatal(err)}users = append(users, u)
}
if err := rows.Err(); err != nil {log.Fatal(err)
}
關鍵點:
- 參數化查詢: 使用
?
(或其他數據庫驅動支持的占位符,如$
1 for PostgreSQL) 作為參數占位符,并將實際參數傳遞給Query
方法,可以有效防止 SQL 注入攻擊。 - 遍歷結果: 使用
for rows.Next()
循環遍歷結果集。 - 讀取數據: 在循環體內,使用
rows.Scan()
將當前行的數據讀入到變量中。 - 錯誤檢查: 在
rows.Next()
循環結束后,務必調用rows.Err()
來檢查遍歷過程中是否發生了錯誤。 - 釋放資源: 使用
defer rows.Close()
來確保結果集被關閉,從而釋放底層的數據庫連接。
查詢單行
使用 db.QueryRow()
或 db.QueryRowContext()
方法執行最多返回一行的查詢。
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
if err != nil {if err == sql.ErrNoRows {// 查無此行,是正常情況fmt.Println("未找到記錄")} else {// 發生了其他錯誤log.Fatal(err)}
}
關鍵點:
QueryRow
總是返回一個*sql.Row
對象(即使沒有找到行)。- 錯誤(包括
sql.ErrNoRows
)會在調用Scan()
方法時返回。因此,需要檢查Scan()
返回的錯誤。
3. 執行非查詢操作
對于 INSERT
, UPDATE
, DELETE
等不返回行的 SQL 語句,使用 db.Exec()
或 db.ExecContext()
方法。
result, err := db.Exec("UPDATE users SET name = ? WHERE id = ?", "New Name", 1)
if err != nil {log.Fatal(err)
}rowsAffected, err := result.RowsAffected()
if err != nil {log.Fatal(err)
}
fmt.Printf("受影響的行數: %d\n", rowsAffected)lastInsertId, err := result.LastInsertId()
if err != nil {// 注意:并非所有數據庫驅動都支持此功能log.Fatal(err)
}
fmt.Printf("最后插入的ID: %d\n", lastInsertId)
高級技巧
1. 預編譯語句 (Prepared Statements)
當需要重復執行相同的 SQL 語句時,使用預編譯語句可以獲得更好的性能,并能提供額外的安全保障。
stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")
if err != nil {log.Fatal(err)
}
defer stmt.Close()_, err = stmt.Exec("Alice", 28)
if err != nil {log.Fatal(err)
}_, err = stmt.Exec("Bob", 32)
if err != nil {log.Fatal(err)
}
2. 事務處理
事務用于將一組操作作為一個原子單元來執行。使用 db.Begin()
開始一個事務。
tx, err := db.Begin()
if err != nil {log.Fatal(err)
}// 在事務中執行操作
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {tx.Rollback() // 出錯時回滾log.Fatal(err)
}_, err = tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err != nil {tx.Rollback() // 出錯時回滾log.Fatal(err)
}// 提交事務
err = tx.Commit()
if err != nil {log.Fatal(err)
}
關鍵點:
- 在
tx
對象上調用Exec
,Query
,QueryRow
等方法來執行事務內的操作。 - 如果任何一步操作失敗,調用
tx.Rollback()
來撤銷所有已執行的操作。 - 所有操作成功后,調用
tx.Commit()
來持久化更改。 - 推薦使用
defer
語句來處理回滾,以確保在函數返回前事務能夠被正確處理:
tx, err := db.Begin()
if err != nil {// ...
}
defer tx.Rollback() // 如果Commit成功,Rollback會返回sql.ErrTxDone,可以忽略
// ... 事務操作
if err = tx.Commit(); err != nil {// ...
}
最佳實踐
- 不要在短函數中打開和關閉數據庫:
sql.DB
被設計為長生命周期的對象。頻繁地Open
和Close
會影響性能。 - 總是檢查錯誤:
database/sql
中的許多操作都會返回錯誤,務必對每一個錯誤進行檢查。 - 使用參數化查詢: 杜絕拼接字符串來構建 SQL 查詢,以防止 SQL 注入。
- 正確關閉
Rows
: 確保sql.Rows
對象在使用完畢后被關閉,以釋放連接。 - 處理
NULL
值: 使用sql.Null*
類型或在Scan
時使用指針類型來處理可能為NULL
的數據庫列。 - 利用
Context
: 對于可能耗時較長的數據庫操作,使用帶有Context
的方法(如QueryContext
),以便在需要時能夠取消操作或設置超時。
小結
database/sql
包為 Go 語言提供了一個強大而靈活的與數據庫交互的工具。通過理解其設計和核心組件,并遵循最佳實踐,開發者可以編寫出健壯、高效且可維護的數據訪問代碼。