database/sql 标准库基础
核心概念
Go 标准库 database/sql 是一个接口与规范层 ,它定义了操作数据库的通用 API(连接池管理、预处理、事务、数据绑定等),但不包含任何具体数据库协议的实现。真正干活的是数据库驱动 ------第三方包通过 sql.Register() 在 init() 中把自己注册进来。
这种设计意味着:无论底层是 MySQL、PostgreSQL 还是 SQLite,上层代码只需面对 database/sql 的统一接口,切换数据库只需换一行 import 和连接字符串。
架构分层
你的应用代码
↓
database/sql (标准库:接口 + 连接池 + 事务抽象)
↓
database/sql/driver (驱动接口定义)
↓
具体驱动 (go-sql-driver/mysql, lib/pq, modernc.org/sqlite ...)
↓
数据库服务器
驱动注册机制
go
import (
"database/sql"
_ "github.com/go-sql-driver/mysql" // 前缀 _ 表示只执行 init()
)
_ "驱动包路径" 是 Go 的惯用写法:不直接引用包内的任何标识符,但触发其 init() 函数。驱动包的 init() 内部调用 sql.Register("mysql", &MySQLDriver{}),把自己注册到 database/sql 的全局驱动表里。
sql.DB:连接池的抽象
sql.Open() 返回的 *sql.DB 不是单个连接,而是一个连接池管理器。它内部维护着一组空闲连接,按需创建和回收。
核心方法一览
| 方法 | 用途 | 返回值 |
|---|---|---|
sql.Open(driver, dsn) |
创建连接池配置(懒连接,不立即建连) | *sql.DB, error |
db.Ping() |
实际建立连接,验证可达性 | error |
db.Close() |
关闭连接池,释放所有资源 | error |
db.Query(sql, args...) |
执行查询,返回多行 | *sql.Rows, error |
db.QueryRow(sql, args...) |
执行查询,返回单行 | *sql.Row(无 error) |
db.Exec(sql, args...) |
执行写操作(INSERT/UPDATE/DELETE) | sql.Result, error |
连接池关键参数
go
db.SetMaxOpenConns(25) // 最大同时打开的连接数(0 = 无限制)
db.SetMaxIdleConns(10) // 最大空闲连接数(默认 2,偏低)
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间
db.SetConnMaxIdleTime(2 * time.Minute) // 连接最大空闲时间
为什么需要 ConnMaxLifetime? 数据库服务器和中间件(如 ProxySQL)通常会主动断开长时间空闲的连接。如果不设生命周期,Go 连接池里的连接可能已经被服务端关闭,下次使用时才会报错。设置一个比服务端超时更短的值,让连接池主动回收。
三种查询姿势
1. Query:多行查询
go
rows, err := db.Query("SELECT id, name FROM users WHERE age > ?", 18)
if err != nil {
log.Fatal(err)
}
defer rows.Close() // 必须关闭,否则连接泄漏
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err := rows.Err(); err != nil { // 检查遍历过程中是否有错
log.Fatal(err)
}
三个必做动作 :defer rows.Close() → rows.Next() 循环 → 最后检查 rows.Err()。遗漏任何一个都是 Bug。
2. QueryRow:单行查询
go
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("用户不存在")
} else {
log.Fatal(err)
}
}
QueryRow 不直接返回 error,error 被延迟到 Scan() 时才暴露。这样设计是为了链式调用。注意 sql.ErrNoRows 是预期内的"没找到",不应当视为系统错误。
3. Exec:写操作
go
result, err := db.Exec("INSERT INTO users(name, age) VALUES(?, ?)", "张三", 25)
if err != nil {
log.Fatal(err)
}
id, _ := result.LastInsertId() // 自增主键
affected, _ := result.RowsAffected() // 影响行数
完整练习代码
以下代码使用 SQLite(modernc.org/sqlite,纯 Go 实现,无需 CGO),可以直接运行。
go
// database_sql_basics.go
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "modernc.org/sqlite"
)
func main() {
// 1. 打开数据库连接池
// sql.Open 不会立即建立连接,只是初始化连接池配置
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
log.Fatalf("打开数据库失败: %v", err)
}
defer db.Close()
// 2. 配置连接池参数
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(10 * time.Minute)
// 3. Ping 验证连接是否可达
if err := db.Ping(); err != nil {
log.Fatalf("数据库连接失败: %v", err)
}
fmt.Println("=== 数据库连接成功 ===")
// 4. 建表
schema := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
if _, err := db.Exec(schema); err != nil {
log.Fatalf("建表失败: %v", err)
}
fmt.Println("=== 建表成功 ===")
// 5. 插入数据 (Exec)
result, err := db.Exec(
"INSERT INTO users(name, email, age) VALUES(?, ?, ?)",
"张三", "zhangsan@example.com", 28,
)
if err != nil {
log.Fatalf("插入失败: %v", err)
}
lastID, _ := result.LastInsertId()
affected, _ := result.RowsAffected()
fmt.Printf("插入成功: ID=%d, 影响行数=%d\n", lastID, affected)
// 批量插入
for _, u := range []struct{ name, email string; age int }{
{"李四", "lisi@example.com", 32},
{"王五", "wangwu@example.com", 25},
{"赵六", "zhaoliu@example.com", 35},
} {
db.Exec("INSERT INTO users(name, email, age) VALUES(?, ?, ?)", u.name, u.email, u.age)
}
// 6. 单行查询 (QueryRow)
fmt.Println("\n=== 单行查询 ===")
var name, email string
var age int
err = db.QueryRow("SELECT name, email, age FROM users WHERE id = ?", 1).Scan(&name, &email, &age)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("未找到 ID=1 的用户")
} else {
log.Fatal(err)
}
} else {
fmt.Printf("ID=1: %s, %s, %d岁\n", name, email, age)
}
// 7. 多行查询 (Query)
fmt.Println("\n=== 多行查询: age > 26 ===")
rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ? ORDER BY age", 26)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id, rAge int
var rName string
if err := rows.Scan(&id, &rName, &rAge); err != nil {
log.Fatal(err)
}
fmt.Printf(" ID=%d, %s, %d岁\n", id, rName, rAge)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
// 8. 更新
fmt.Println("\n=== 更新操作 ===")
res, err := db.Exec("UPDATE users SET age = ? WHERE name = ?", 29, "张三")
if err != nil {
log.Fatal(err)
}
fmt.Printf("更新影响行数: %d\n", affected2(res))
// 9. 删除
fmt.Println("\n=== 删除操作 ===")
delRes, err := db.Exec("DELETE FROM users WHERE name = ?", "赵六")
if err != nil {
log.Fatal(err)
}
fmt.Printf("删除影响行数: %d\n", affected2(delRes))
// 10. 查看最终数据
fmt.Println("\n=== 最终用户列表 ===")
rows2, _ := db.Query("SELECT id, name, email, age FROM users ORDER BY id")
defer rows2.Close()
for rows2.Next() {
var id, rAge int
var rName, rEmail string
rows2.Scan(&id, &rName, &rEmail, &rAge)
fmt.Printf(" [%d] %s <%s> %d岁\n", id, rName, rEmail, rAge)
}
}
func affected2(res sql.Result) int64 {
n, _ := res.RowsAffected()
return n
}
运行方式
bash
mkdir -p demo && cd demo
go mod init demo
go get modernc.org/sqlite
# 将上面的代码保存为 main.go,然后:
go run main.go
要点总结
database/sql是接口层 ,真正通信靠驱动包;_ "驱动路径"触发init()完成注册sql.DB是连接池 ,不是单个连接;sql.Open()是懒连接,db.Ping()才真正建连- 连接池四个参数 必须根据业务场景合理配置,特别是
ConnMaxLifetime要比 DB 服务端超时短 - Query/QueryRow/Exec 三板斧 :Query 返回
*sql.Rows(必须 Close),QueryRow 返回*sql.Row(Scan 时才报错),Exec 返回sql.Result(可取 LastInsertId/RowsAffected) defer rows.Close()不是可选项------遗漏会导致连接泄漏,连接池耗尽后整个服务卡死