1 database/sql 接口
Go 的 database/sql 包定义了一套通用数据库接口,具体的数据库驱动通过匿名导入注册。这种设计使得切换数据库只需更换驱动,业务代码几乎无需修改。
连接数据库
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// DSN 格式: user:password@tcp(host:port)/dbname?params
dsn := "root:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("打开数据库失败:", err)
}
defer db.Close()
// sql.Open 不会真正建立连接,需要 Ping 验证
if err := db.Ping(); err != nil {
log.Fatal("连接数据库失败:", err)
}
fmt.Println("数据库连接成功!")
}
匿名导入:_ "github.com/go-sql-driver/mysql" 中的下划线 _ 表示只执行包的 init() 函数来注册驱动,不直接使用包中的符号。
安装 MySQL 驱动
go get -u github.com/go-sql-driver/mysql
🔄 Go database/sql vs PHP PDO vs Node.js mysql2
// Go:统一接口 + 驱动匿名导入
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
db, err := sql.Open("mysql", dsn)
# PHP PDO:统一接口 + DSN
# $pdo = new PDO("mysql:host=127.0.0.1;dbname=myapp", "root", "password");
# Node.js mysql2:专用库
# const mysql = require('mysql2/promise');
# const conn = await mysql.createConnection({host: '127.0.0.1', ...});
相似点:Go 的 database/sql 类似 PHP 的 PDO——都提供统一抽象接口,支持多种数据库后端。
2 CRUD 操作
查询多行 — db.Query
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email FROM users WHERE active = ?", true)
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.Email); err != nil {
return nil, err
}
users = append(users, u)
}
// 检查遍历过程中是否发生错误
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
查询单行 — db.QueryRow
func getUserByID(db *sql.DB, id int) (*User, error) {
var u User
err := db.QueryRow(
"SELECT id, name, email FROM users WHERE id = ?", id,
).Scan(&u.ID, &u.Name, &u.Email)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("用户 %d 不存在", id)
}
if err != nil {
return nil, err
}
return &u, nil
}
插入 — db.Exec
func createUser(db *sql.DB, name, email string) (int64, error) {
result, err := db.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)", name, email,
)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
更新与删除
func updateUser(db *sql.DB, id int, name, email string) error {
result, err := db.Exec(
"UPDATE users SET name = ?, email = ? WHERE id = ?", name, email, id,
)
if err != nil {
return err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return fmt.Errorf("用户 %d 不存在", id)
}
return nil
}
func deleteUser(db *sql.DB, id int) error {
result, err := db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
return err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return fmt.Errorf("用户 %d 不存在", id)
}
return nil
}
关键区别:db.Query 返回 *sql.Rows(需遍历和关闭);db.QueryRow 返回 *sql.Row(单行,自动关闭);db.Exec 返回 sql.Result(用于增删改)。
3 预处理语句
预处理语句(Prepared Statement)在需要重复执行相同 SQL 时能显著提升性能,同时天然防止 SQL 注入。
func batchInsertUsers(db *sql.DB, users []User) error {
stmt, err := db.Prepare("INSERT INTO users (name, email) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
for _, u := range users {
_, err := stmt.Exec(u.Name, u.Email)
if err != nil {
return fmt.Errorf("插入用户 %s 失败: %w", u.Name, err)
}
}
return nil
}
func searchUsers(db *sql.DB, keyword string) ([]User, error) {
stmt, err := db.Prepare("SELECT id, name, email FROM users WHERE name LIKE ?")
if err != nil {
return nil, err
}
defer stmt.Close()
rows, err := stmt.Query("%" + keyword + "%")
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.Email); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
SQL 注入防护:永远使用 ? 占位符传递参数,绝不要用 fmt.Sprintf 拼接 SQL 字符串。db.Query、db.Exec 的参数化查询也同样安全。
4 事务
事务确保一组数据库操作要么全部成功,要么全部回滚。Go 中使用 db.Begin() 开启事务。
基本事务流程
func transferMoney(db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
// defer 确保异常时回滚
defer tx.Rollback()
// 扣款
_, err = tx.Exec(
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
amount, fromID, amount,
)
if err != nil {
return fmt.Errorf("扣款失败: %w", err)
}
// 入账
_, err = tx.Exec(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, toID,
)
if err != nil {
return fmt.Errorf("入账失败: %w", err)
}
// 记录转账日志
_, err = tx.Exec(
"INSERT INTO transfer_log (from_id, to_id, amount) VALUES (?, ?, ?)",
fromID, toID, amount,
)
if err != nil {
return fmt.Errorf("记录日志失败: %w", err)
}
// 全部成功,提交事务
return tx.Commit()
}
封装通用事务函数
func withTransaction(db *sql.DB, fn func(tx *sql.Tx) error) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
}
return tx.Commit()
}
// 使用示例
func createUserWithProfile(db *sql.DB, name, email, bio string) error {
return withTransaction(db, func(tx *sql.Tx) error {
result, err := tx.Exec(
"INSERT INTO users (name, email) VALUES (?, ?)", name, email,
)
if err != nil {
return err
}
userID, _ := result.LastInsertId()
_, err = tx.Exec(
"INSERT INTO profiles (user_id, bio) VALUES (?, ?)", userID, bio,
)
return err
})
}
defer + Rollback 模式:在 defer tx.Rollback() 后调用 tx.Commit()——如果 Commit 成功,后续的 Rollback 是空操作(不会报错);如果中途返回错误,defer 会自动回滚。
5 连接池与最佳实践
sql.DB 内部自带连接池,通过以下方法配置:
package main
import (
"database/sql"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
func initDB() *sql.DB {
db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/myapp?charset=utf8mb4&parseTime=True")
if err != nil {
log.Fatal(err)
}
// 最大打开连接数(默认无限制)
db.SetMaxOpenConns(25)
// 最大空闲连接数(默认 2)
db.SetMaxIdleConns(10)
// 连接最大存活时间(建议小于数据库的 wait_timeout)
db.SetConnMaxLifetime(5 * time.Minute)
// 空闲连接最大存活时间
db.SetConnMaxIdleTime(3 * time.Minute)
if err := db.Ping(); err != nil {
log.Fatal(err)
}
return db
}
func main() {
db := initDB()
defer db.Close()
// 查看连接池状态
stats := db.Stats()
log.Printf("打开连接: %d, 使用中: %d, 空闲: %d",
stats.OpenConnections, stats.InUse, stats.Idle)
}
💡 数据库最佳实践
✓ sql.DB 全局唯一,通过依赖注入传递
✓ 始终检查 rows.Err() 和 rows.Close()
✓ 使用 ? 占位符防 SQL 注入
✓ 配置合理的连接池参数
✗ 不要每次请求都 sql.Open
✗ 不要忘记 defer rows.Close()
✗ 不要用字符串拼接 SQL
✗ 不要在 for 循环中打开连接
6 本章要点
🔌 database/sql
- •
sql.Open+ 匿名导入驱动 - •
db.Ping()验证连接 - • 统一接口,可切换数据库
📝 CRUD
- •
db.Query查询多行 - •
db.QueryRow查询单行 - •
db.Exec增删改操作
🔒 安全与事务
- • 预处理语句防 SQL 注入
- •
db.Begin/tx.Commit/tx.Rollback - • defer + Rollback 安全模式
⚙️ 连接池
- •
SetMaxOpenConns最大连接数 - •
SetMaxIdleConns空闲连接数 - •
SetConnMaxLifetime存活时间
🛡️ 最佳实践
- • 全局单例
*sql.DB - • 始终
defer rows.Close() - • 参数化查询而非拼接
📦 常用驱动
- • MySQL: go-sql-driver/mysql
- • PostgreSQL: lib/pq, pgx
- • SQLite: mattn/go-sqlite3