Appearance
21|数据库基础
go-gateway 的管理后台需要持久化路由配置。路由规则不能只在内存里存一份,进程重启后就丢了。数据库负责把配置持久化到磁盘,让网关重启后能从数据库恢复路由表。Go 标准库提供了 database/sql 包,定义了统一的 SQL 接口,配合数据库驱动(如 sqlite、mysql)就能操作关系型数据库。
一、database/sql 概览
database/sql 不直接实现任何数据库协议,它只定义接口。具体操作需要引入对应的数据库驱动:
bash
go get github.com/mattn/go-sqlite3 # SQLite
go get -u github.com/go-sql-driver/mysql # MySQL使用时先导入驱动(用 _ 匿名导入,只执行驱动的 init 函数注册自己):
go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)二、连接数据库
go
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./gateway.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
log.Fatal(err)
}
fmt.Println("connected")
}sql.Open 只是创建一个数据库对象,不会立即建立连接。db.Ping() 才真正尝试连接数据库,用于验证连接参数是否正确。defer db.Close() 在函数返回前关闭连接池——但 database/sql 的连接池是惰性的,Close 只是标记不再使用,不会立刻断开所有连接。
MySQL 连接字符串:
go
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/gateway?charset=utf8mb4&parseTime=True")三、创建表
路由配置表的结构:
go
func initSchema(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prefix TEXT NOT NULL UNIQUE,
backend TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
_, err := db.Exec(query)
return err
}db.Exec 执行不返回结果集的 SQL(CREATE、INSERT、UPDATE、DELETE)。返回值是 sql.Result,包含受影响行数和最后插入的 ID。
四、插入数据
go
func addRoute(db *sql.DB, prefix, backend string) error {
_, err := db.Exec(
"INSERT INTO routes (prefix, backend) VALUES (?, ?)",
prefix, backend,
)
return err
}? 是占位符,database/sql 会自动处理转义,防止 SQL 注入。不同数据库的占位符语法不同:MySQL 用 ?,PostgreSQL 用 $1、$2,SQLite 用 ? 或 $1。database/sql 统一用 ?,驱动层负责转换。
五、查询数据
go
func listRoutes(db *sql.DB) ([]Route, error) {
rows, err := db.Query("SELECT id, prefix, backend FROM routes")
if err != nil {
return nil, err
}
defer rows.Close()
var routes []Route
for rows.Next() {
var r Route
if err := rows.Scan(&r.ID, &r.Prefix, &r.Backend); err != nil {
return nil, err
}
routes = append(routes, r)
}
return routes, rows.Err()
}db.Query 返回 *sql.Rows,遍历用 rows.Next(),每行用 rows.Scan 把列值扫描到变量里。rows.Close() 必须调用,否则连接不会回收到连接池。用 defer rows.Close() 是最安全的做法。
查询完所有行后,还要检查 rows.Err()。rows.Next() 返回 false 可能是因为遍历完了,也可能是因为发生了错误。不检查 rows.Err() 会漏掉遍历过程中的错误。
查询单行用 db.QueryRow:
go
func getRoute(db *sql.DB, id int) (*Route, error) {
var r Route
err := db.QueryRow(
"SELECT id, prefix, backend FROM routes WHERE id = ?", id,
).Scan(&r.ID, &r.Prefix, &r.Backend)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &r, nil
}sql.ErrNoRows 表示查询结果为空,这不是错误,只是没找到数据。返回 nil, nil 让调用方判断记录是否存在。
六、预处理语句
频繁执行的 SQL 应该预处理,数据库只解析一次,后续执行直接复用执行计划:
go
stmt, err := db.Prepare("INSERT INTO routes (prefix, backend) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
for _, route := range routes {
_, err := stmt.Exec(route.Prefix, route.Backend)
if err != nil {
return err
}
}db.Prepare 返回 *sql.Stmt,可以多次执行。defer stmt.Close() 释放预处理语句占用的数据库资源。
七、事务
多条 SQL 需要原子性执行时用事务。要么全部成功,要么全部回滚:
go
func batchAddRoutes(db *sql.DB, routes []Route) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // 如果提交成功,Rollback 不会生效
for _, route := range routes {
_, err := tx.Exec("INSERT INTO routes (prefix, backend) VALUES (?, ?)",
route.Prefix, route.Backend)
if err != nil {
return err // 自动回滚(因为 defer 了 Rollback)
}
}
return tx.Commit()
}事务对象 tx 有自己的 Exec、Query 方法,和 db 的方法签名一样。事务里的操作在提交前对其他连接不可见。defer tx.Rollback() 是一个安全网——如果函数中途返回错误,事务自动回滚;如果 tx.Commit() 成功,Rollback 变成空操作。
八、连接池
database/sql 内部维护了一个连接池,默认行为:
| 参数 | 默认值 | 说明 |
|---|---|---|
MaxOpenConns | 无限制 | 最大打开连接数 |
MaxIdleConns | 2 | 最大空闲连接数 |
ConnMaxLifetime | 0(不限制) | 连接最大存活时间 |
生产环境应该根据并发量调整:
go db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)连接数设置过小,并发请求会排队等待;设置过大,数据库端压力增加。通常从低值开始压测,逐步调整。
九、常见错误
忘记 rows.Close()
go
rows, _ := db.Query("SELECT ...")
// 忘了 rows.Close()rows 不关闭,底层连接不会回收到连接池。并发量稍高就会耗尽连接数,后续查询卡住。
在循环里用 db.Query
go
for _, id := range ids {
row := db.QueryRow("SELECT ... WHERE id = ?", id)
// ...
}每条查询都创建新的预处理语句和执行计划。应该用 db.Prepare 预处理一次,循环里用 stmt.QueryRow。
混淆 nil 和 sql.ErrNoRows
go
var r Route
err := db.QueryRow("...").Scan(&r.ID)
if err != nil {
return err // 错误:sql.ErrNoRows 不应该当成错误返回
}sql.ErrNoRows 是正常结果(查询为空),应该单独处理。
事务里用 db 而不是 tx
go
tx, _ := db.Begin()
tx.Exec("INSERT ...")
db.Exec("INSERT ...") // 错误:这条不在事务里事务里的所有操作必须用 tx 执行,用 db 会创建新的连接,不受事务控制。