znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第16章 - 数据库编程

数据持久化是后端开发的核心需求。本章讲解 Go 操作数据库的方式:标准库 database/sql、主流驱动、ORM 框架,以及连接池、事务、预处理等关键概念。

16.1 database/sql 概述

database/sql 是 Go 标准库提供的数据库访问抽象层。它本身不实现具体数据库的通信协议,而是定义了一套通用接口,由各数据库的驱动实现。这种设计使得切换数据库时业务代码改动最小。

常见数据库驱动:

数据库 驱动包
MySQL github.com/go-sql-driver/mysql
PostgreSQL github.com/lib/pqgithub.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.Registersql.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 的方法(QueryContextExecContextBeginTx)可实现查询超时与取消:

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/sqlxdatabase/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 和缓存客户端:

  • Redisgithub.com/redis/go-redis
  • MongoDBgo.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 的工程化实践与综合项目实战。