第16章 - 数据库编程
数据持久化是后端开发的核心需求。本章讲解 Go 操作数据库的方式:标准库 database/sql、主流驱动、ORM 框架,以及连接池、事务、预处理等关键概念。
16.1 database/sql 概述
database/sql 是 Go 标准库提供的数据库访问抽象层。它本身不实现具体数据库的通信协议,而是定义了一套通用接口,由各数据库的驱动实现。这种设计使得切换数据库时业务代码改动最小。
常见数据库驱动:
| 数据库 | 驱动包 |
|---|---|
| MySQL | github.com/go-sql-driver/mysql |
| PostgreSQL | github.com/lib/pq 或 github.com/jackc/pgx |
| SQLite | github.com/mattn/go-sqlite3 |
| SQL Server | github.com/microsoft/go-mssqldb |
16.2 连接数据库
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 空白导入,注册驱动
)
func main() {
// dsn 格式: 用户名:密码@tcp(主机:端口)/数据库名?参数
dsn := "user:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=true"
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)
}
}
注意:驱动通过空白导入
_注册,其init函数会调用sql.Register。sql.Open的第一个参数即注册的驱动名。
16.3 连接池配置
*sql.DB 实际上是一个连接池,并发安全,应在程序生命周期内复用同一个实例(而非每次操作都 Open):
db.SetMaxOpenConns(25) // 最大打开连接数
db.SetMaxIdleConns(25) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(time.Minute) // 连接最大空闲时间
合理配置连接池对高并发服务的稳定性至关重要。连接数过小会限制吞吐,过大会压垮数据库。
16.4 执行查询
16.4.1 查询单行
type User struct {
ID int
Name string
Age int
}
func getUser(db *sql.DB, id int) (*User, error) {
var u User
// 使用占位符防止 SQL 注入(MySQL 用 ?,PostgreSQL 用 $1)
row := db.QueryRow("SELECT id, name, age FROM users WHERE id = ?", id)
err := row.Scan(&u.ID, &u.Name, &u.Age)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("用户 %d 不存在", id)
}
return nil, err
}
return &u, nil
}
16.4.2 查询多行
func listUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 18)
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
}
关键点:查询多行后必须
defer rows.Close(),并在循环后检查rows.Err()。
16.5 增删改操作
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
}
id, err := result.LastInsertId() // 获取自增主键
return id, err
}
func updateAge(db *sql.DB, id, age int) error {
result, err := db.Exec("UPDATE users SET age = ? WHERE id = ?", age, id)
if err != nil {
return err
}
rows, _ := result.RowsAffected() // 受影响行数
fmt.Printf("更新了 %d 行\n", rows)
return nil
}
16.6 防止 SQL 注入
始终使用参数化查询(占位符),永远不要用字符串拼接构造 SQL:
// 危险!存在 SQL 注入漏洞
db.Query("SELECT * FROM users WHERE name = '" + name + "'")
// 安全:使用占位符,参数由驱动安全转义
db.Query("SELECT * FROM users WHERE name = ?", name)
参数化查询不仅防注入,还能利用预处理提升性能。
16.7 预处理语句
对于需要反复执行的 SQL,预处理(Prepared Statement)可提升性能并增强安全:
stmt, err := db.Prepare("INSERT INTO users (name, age) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
for _, u := range users {
if _, err := stmt.Exec(u.Name, u.Age); err != nil {
return err
}
}
16.8 事务
事务保证一组操作的原子性——要么全部成功,要么全部回滚:
func transfer(db *sql.DB, from, to int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
// 确保异常时回滚
defer tx.Rollback() // 若已 Commit,Rollback 无副作用
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err // defer 中的 Rollback 会执行
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return tx.Commit() // 提交事务
}
模式:
Begin后立即defer tx.Rollback(),成功路径以tx.Commit()结尾。Commit 之后的 Rollback 是无操作,因此这个模式既安全又简洁。
16.9 使用 Context 控制超时
带 Context 的方法(QueryContext、ExecContext、BeginTx)可实现查询超时与取消:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT ... FROM big_table")
生产环境强烈建议所有数据库操作都传入 context,避免慢查询拖垮服务。
16.10 ORM 与查询构建器
直接使用 database/sql 较为底层,需要手写 SQL 和扫描字段。社区提供了更高层的工具:
16.10.1 GORM
最流行的 Go ORM,支持模型映射、关联、钩子、自动迁移:
import "gorm.io/gorm"
type User struct {
gorm.Model // 内置 ID、CreatedAt、UpdatedAt、DeletedAt
Name string
Age int
}
db.AutoMigrate(&User{}) // 自动建表
db.Create(&User{Name: "Alice", Age: 30}) // 插入
var user User
db.First(&user, 1) // 主键查询
db.Where("age > ?", 18).Find(&users) // 条件查询
db.Model(&user).Update("Age", 31) // 更新
db.Delete(&user) // 软删除
16.10.2 sqlx
jmoiron/sqlx 是 database/sql 的轻量扩展,保留原生性能的同时简化字段扫描:
import "github.com/jmoiron/sqlx"
var users []User
db.Select(&users, "SELECT * FROM users WHERE age > ?", 18) // 自动映射到结构体
16.10.3 sqlc 与 ent
- sqlc:根据 SQL 文件生成类型安全的 Go 代码,兼顾性能与安全。
- ent:Facebook 开源的实体框架,图式化建模,代码生成。
16.10.4 选型建议
| 方案 | 适用场景 |
|---|---|
database/sql |
追求极致控制和性能,简单查询 |
sqlx |
想保留原生性能又简化扫描 |
| GORM | 快速开发,复杂关联,团队熟悉 ORM |
| sqlc | 喜欢写 SQL,又要类型安全 |
16.11 NoSQL 与缓存
Go 也有成熟的 NoSQL 和缓存客户端:
- Redis:
github.com/redis/go-redis - MongoDB:
go.mongodb.org/mongo-driver - Elasticsearch:官方
go-elasticsearch
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
rdb.Set(ctx, "key", "value", time.Hour)
val, err := rdb.Get(ctx, "key").Result()
16.12 本章小结
本章讲解了 Go 的数据库编程:标准库 database/sql 提供了统一的数据库抽象,配合各驱动操作不同数据库。我们学习了连接池配置、查询、增删改、参数化查询防注入、预处理、事务和 context 超时控制等核心技能。对于更高层抽象,GORM、sqlx、sqlc 等工具各有所长。掌握这些知识,你能构建出健壮的数据持久化层。
下一章我们将学习 Go 的工程化实践与综合项目实战。