database/sql
问题
Go 标准库的 database/sql 怎么用?如何管理连接池和事务?
答案
基本使用
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 注册 MySQL 驱动
)
// 打开连接(不会立即建立连接)
db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/dbname?parseTime=true")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 验证连接
if err := db.Ping(); err != nil {
log.Fatal(err)
}
// 连接池配置
db.SetMaxOpenConns(100)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
CRUD 操作
// 查询单行
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?", userID,
).Scan(&user.ID, &user.Name, &user.Email)
if err == sql.ErrNoRows {
// 未找到
} else if err != nil {
// 其他错误
}
// 查询多行
rows, err := db.QueryContext(ctx,
"SELECT id, name FROM users WHERE age > ?", 18,
)
if err != nil { return err }
defer rows.Close() // 必须 Close!
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return err
}
users = append(users, u)
}
// 检查迭代错误
if err := rows.Err(); err != nil {
return err
}
// 插入
result, err := db.ExecContext(ctx,
"INSERT INTO users (name, email) VALUES (?, ?)", name, email,
)
id, _ := result.LastInsertId()
affected, _ := result.RowsAffected()
// 更新
result, err := db.ExecContext(ctx,
"UPDATE users SET name = ? WHERE id = ?", name, id,
)
SQL 注入防御
// ❌ 字符串拼接——SQL 注入风险!
query := "SELECT * FROM users WHERE name = '" + name + "'"
// ✅ 参数化查询——永远用 ?(MySQL)或 $1(PostgreSQL)
db.QueryContext(ctx, "SELECT * FROM users WHERE name = ?", name)
事务
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
// defer + 错误处理确保事务不会挂起
defer tx.Rollback() // 如果已 Commit 则 Rollback 是 no-op
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID,
)
if err != nil { return err }
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID,
)
if err != nil { return err }
return tx.Commit()
预编译语句
stmt, err := db.PrepareContext(ctx, "SELECT * FROM users WHERE id = ?")
if err != nil { return err }
defer stmt.Close()
// 多次执行同一语句
for _, id := range userIDs {
var user User
stmt.QueryRowContext(ctx, id).Scan(&user.ID, &user.Name)
}
常见面试问题
Q1: rows.Close() 不调用会怎样?
答案:数据库连接不会归还连接池,导致连接泄漏。连接池中的可用连接逐渐减少,最终所有请求都在等待连接。
Q2: database/sql 和 ORM 怎么选?
答案:
- 性能敏感、SQL 简单 →
database/sql或sqlx - 快速开发、模型复杂 →
GORM - 类型安全、复杂查询 →
sqlc(SQL 生成 Go 代码)