database #
Go 语言通过内置的 database/sql 包支持关系型数据库的操作,同时也支持通过第三方库与 NoSQL 数据库进行交互。你可以通过标准的 SQL 操作与关系型数据库(如 MySQL、PostgreSQL)进行交互,也可以使用专门的库来连接 NoSQL 数据库(如 MongoDB、Redis)。
database/sql 包通过提供统一的编程接口,实现了对不同数据库驱动的抽象。
原理 #
Driver接口定义:database/sql/driver包中定义了一个Driver接口,该接口用于表示一个数据库驱动。驱动开发者需要实现该接口来提供与特定数据库的交互能力。Driver注册:驱动开发者需要在程序初始化阶段,通过调用database/sql包提供的sql.Register()方法将自己的驱动注册到database/sql中。这样,database/sql就能够识别和使用该驱动。- 数据库连接池管理:
database/sql维护了一个数据库连接池,用于管理数据库连接。当通过sql.Open()打开一个数据库连接时,database/sql会在合适的时机调用注册的驱动来创建一个具体的连接,并将其添加到连接池中。连接池会负责连接的复用、管理和维护工作,并且这是并发安全的。 - 统一的编程接口:
database/sql定义了一组统一的编程接口供用户使用,如Prepare()、Exec()和Query()等方法,用于准备 SQL 语句、执行 SQL 语句和执行查询等操作。这些方法会接收参数并调用底层驱动的相应方法来执行实际的数据库操作。 - 接口方法的实现:驱动开发者需要实现
database/sql/driver中定义的一些接口方法,以此来支持上层database/sql包提供的Prepare()、Exec()和Query()等方法,以提供底层数据库的具体实现。当database/sql调用这些方法时,实际上会调用注册的驱动的相应方法来执行具体的数据库操作。
常用方法 #
数据库连接 #
| 方法名 | 描述 | 示例 |
|---|---|---|
sql.Open() |
打开数据库连接 | db, err := sql.Open("mysql", dsn) |
db.Ping() |
测试数据库连接是否有效 | err = db.Ping() |
db.Close() |
关闭数据库连接 | defer db.Close() |
连接池配置 #
| 方法名 | 描述 | 示例 |
|---|---|---|
db.SetMaxOpenConns(max int) |
设置最大打开连接数(默认无限制),避免数据库过载。 | db.SetMaxOpenConns(10) |
db.SetMaxIdleConns(max int) |
设置最大空闲连接数(默认2),减少短连接频繁创建开销。 | db.SetMaxIdleConns(2) |
db.SetConnMaxLifetime(d time.Duration) |
设置连接最大存活时间,避免数据库主动断开(如 MySQL 的 wait_timeout)。 |
db.SetConnMaxLifetime(3 * time.Second) |
db.SetConnMaxIdleTime(d time.Duration) |
Go 1.15+新增,设置空闲连接最大保留时间,防止长期闲置占用资源。 |
db.SetConnMaxIdleTime(3 * time.Second) |
事务方法 #
| 方法名 | 描述 | 示例 |
|---|---|---|
db.Begin() |
开始一个事务 | tx, err := db.Begin() |
tx.Rollback() |
回滚事务 | tx.Rollback() |
tx.Commit() |
提交事务 | err = tx.Commit() |
查询和执行方法 #
| 方法名 | 描述 | 示例 |
|---|---|---|
tx.Exec() |
执行不返回结果的SQL语句,用于CREATE、INSERT、UPDATE、DELETE等操作 | tx.Exec("create table ...") |
tx.Query() |
执行返回多行结果的SQL查询 | rows, err := tx.Query("select ...") |
tx.QueryRow() |
执行返回单行结果的SQL查询 | tx.QueryRow("select ...") |
stmt.Exec() |
使用预处理语句执行SQL语句 | stmt.Exec("f", "g") |
预处理语句 #
| 方法名 | 描述 | 示例 |
|---|---|---|
tx.Prepare() |
创建预处理语句 | stmt, err := tx.Prepare(...) |
stmt.Close() |
关闭预处理语句 | defer stmt.Close() |
查询结果处理 #
| 方法名 | 描述 | 示例 |
|---|---|---|
rows.Next() |
逐行迭代查询结果 | rows.Next() |
rows.Scan() |
将当前行的列值赋值给变量 | rows.Scan(&s1, &s2) |
rows.Err() |
检查查询和迭代过程中的错误 | rows.Err() |
rows.Close() |
关闭结果集,释放相关资源 | defer rows.Close() |
结果集 #
新增、更新、删除时返回的结果集接口定义如下:
type Result interface {
// 返回数据库响应命令生成的整数。
// 通常,当插入新行时,这将来自“自动递增”列。
// 并非所有数据库都支持此功能,而且此类语句的语法也各不相同。
LastInsertId() (int64, error)
// 返回受更新、插入或删除影响的行数。
// 并非每个数据库或数据库驱动程序都支持此功能。
RowsAffected() (int64, error)
}
数据库驱动 #
常用驱动 #
MySQL #
github.com/go-sql-driver/mysql
Oracle #
github.com/godror/godror
PostgreSQL #
github.com/lib/pq
MongoDB #
go.mongodb.org/mongo-driver/mongo
SQLite3 #
modernc.org/sqlite- 作为一个较新的库,它的社区支持和文档可能不如 go-sqlite3 庞大。
- 由于它是纯 Go 实现,它在一些特殊场景下可能会出现一些性能瓶颈,但这些问题通常会在未来的版本中逐步得到解决。
github.com/mattn/go-sqlite3- 这是 Go 中最常用的 SQLite 库之一,社区活跃,维护频繁。
- 因为它依赖于 SQLite 的 C 库,它的功能和性能通常比其他 Go SQLite 库更为可靠。
- 需要 C 编译器,因此在某些系统或平台上(特别是轻量级系统或容器环境)可能会遇到安装和配置的问题。
下载驱动 #
# mysql驱动
go get github.com/go-sql-driver/mysql
加载驱动 #
由于驱动在init方法中通过调用 database/sql 包提供的 sql.Register() 方法将自己的驱动注册到 database/sql 中。所以我们只需要匿名引入即可。
mysql驱动内部的注册方法如下:
func init() {
if driverName != "" {
sql.Register(driverName, &MySQLDriver{})
}
}
加载驱动:
import _ "github.com/go-sql-driver/mysql"
连接数据库 #
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // 仅导入驱动,不直接使用
)
func main() {
// 数据源名称(DSN)格式:用户名:密码@协议(地址)/数据库?参数=值
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True"
// 打开数据库连接
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("连接数据库失败:", err)
}
defer db.Close() // 确保在函数退出时关闭数据库连接
// 验证连接
if err := db.Ping(); err != nil {
log.Fatal("Ping数据库失败:", err)
}
fmt.Println("成功连接到数据库!")
}
基本查询操作 #
执行简单查询 #
func queryExample(db *sql.DB) {
// 查询单行
var name string
var age int
row := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1)
err := row.Scan(&name, &age)
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("未找到记录")
return
}
log.Fatal("查询失败:", err)
}
fmt.Printf("用户: %s, 年龄: %d\n", name, age)
}
查询多行数据 #
func queryMultipleRows(db *sql.DB) {
// 查询多行
rows, err := db.Query("SELECT id, name, age FROM users WHERE age > ?", 18)
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows.Close() // 非常重要:确保关闭rows
// 遍历结果集
for rows.Next() {
var id int
var name string
var age int
if err := rows.Scan(&id, &name, &age); err != nil {
log.Fatal("Scan失败:", err)
}
fmt.Printf("ID: %d, 名称: %s, 年龄: %d\n", id, name, age)
}
// 检查遍历过程中的错误
if err := rows.Err(); err != nil {
log.Fatal("结果集遍历失败:", err)
}
}
修改数据操作 #
插入数据 #
func insertExample(db *sql.DB) {
// 插入单条记录
result, err := db.Exec(
"INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
"张三", 30, "zhangsan@example.com",
)
if err != nil {
log.Fatal("插入失败:", err)
}
// 获取自增ID
id, err := result.LastInsertId()
if err != nil {
log.Fatal("获取最后插入ID失败:", err)
}
// 获取受影响行数
rows, err := result.RowsAffected()
if err != nil {
log.Fatal("获取受影响行数失败:", err)
}
fmt.Printf("成功插入记录,ID: %d, 受影响行数: %d\n", id, rows)
}
更新数据 #
func updateExample(db *sql.DB) {
// 更新记录
result, err := db.Exec(
"UPDATE users SET age = ?, email = ? WHERE id = ?",
31, "zhangsan_new@example.com", 1,
)
if err != nil {
log.Fatal("更新失败:", err)
}
// 获取受影响行数
rows, err := result.RowsAffected()
if err != nil {
log.Fatal("获取受影响行数失败:", err)
}
fmt.Printf("成功更新记录,受影响行数: %d\n", rows)
}
删除数据 #
func deleteExample(db *sql.DB) {
// 删除记录
result, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
if err != nil {
log.Fatal("删除失败:", err)
}
// 获取受影响行数
rows, err := result.RowsAffected()
if err != nil {
log.Fatal("获取受影响行数失败:", err)
}
fmt.Printf("成功删除记录,受影响行数: %d\n", rows)
}
预处理语句(Prepared Statements) #
预处理语句是指在数据库中提前编译和优化的SQL语句模板,可以在之后多次重复使用。预处理语句的主要优点如下:
- 提高效率:数据库可以提前编译和优化预处理语句,减少每次执行SQL时的解析时间,特别是在需要多次执行相同SQL语句时。
- 防止SQL注入:通过参数化的SQL语句,用户输入的数据不会直接嵌入到SQL语句中,降低了SQL注入的风险。
- 减少网络开销:在需要多次执行相同的SQL语句时,客户端只需要发送参数,不需要每次都发送完整的SQL语句,减少网络通信的数据量。
在Go语言中,使用预处理语句的基本步骤如下:
- 准备预处理语句:使用
tx.Prepare()方法创建一个预处理语句对象stmt。 - 执行预处理语句:使用
stmt.Exec()方法执行预处理语句,传递参数。 - 关闭预处理语句:执行完毕后,使用
stmt.Close()方法释放相关资源。
// 创建预处理语句
stmt, err := Db.Prepare("insert into student (obj_id,name,age) values(?,?,?)")
if err != nil {
fmt.Println(err)
}
//传参并执行
insertResult, err := stmt.Exec("1234", "lucy", 18)
if err != nil {
fmt.Println(err)
}
fmt.Println(insertResult.LastInsertId())
fmt.Println(insertResult.RowsAffected())
事务处理 #
事务允许将多个数据库操作作为一个原子单元执行,要么全部成功,要么全部失败。
func transactionExample(db *sql.DB) {
// 开始事务
tx, err := db.Begin()
if err != nil {
log.Fatal("开始事务失败:", err)
}
// 使用defer和recover处理异常,确保事务正确结束
defer func() {
if r := recover(); r != nil {
tx.Rollback()
log.Printf("发生异常,事务回滚: %v", r)
}
}()
// 执行第一个操作
_, err = tx.Exec("INSERT INTO accounts (user_id, balance) VALUES (?, ?)", 1, 1000)
if err != nil {
tx.Rollback()
log.Fatal("第一个操作失败,事务回滚:", err)
return
}
// 执行第二个操作
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE user_id = ?", 200, 1)
if err != nil {
tx.Rollback()
log.Fatal("第二个操作失败,事务回滚:", err)
return
}
// 执行第三个操作
_, err = tx.Exec("INSERT INTO transactions (user_id, amount, type) VALUES (?, ?, ?)", 1, 200, "withdrawal")
if err != nil {
tx.Rollback()
log.Fatal("第三个操作失败,事务回滚:", err)
return
}
// 提交事务
err = tx.Commit()
if err != nil {
log.Fatal("提交事务失败:", err)
return
}
fmt.Println("事务成功完成!")
}
处理NULL值 #
数据库中的NULL值需要使用特殊的类型处理:
import (
"database/sql"
"fmt"
"log"
"time"
)
func nullValueExample(db *sql.DB) {
// 声明可能包含NULL的变量
var (
id int
name string
age sql.NullInt64
email sql.NullString
createdAt sql.NullTime
)
// 查询可能包含NULL值的记录
row := db.QueryRow("SELECT id, name, age, email, created_at FROM users WHERE id = ?", 1)
err := row.Scan(&id, &name, &age, &email, &createdAt)
if err != nil {
log.Fatal("查询失败:", err)
}
// 安全地访问可能为NULL的值
fmt.Printf("ID: %d, 名称: %s\n", id, name)
if age.Valid {
fmt.Printf("年龄: %d\n", age.Int64)
} else {
fmt.Println("年龄: NULL")
}
if email.Valid {
fmt.Printf("邮箱: %s\n", email.String)
} else {
fmt.Println("邮箱: NULL")
}
if createdAt.Valid {
fmt.Printf("创建时间: %v\n", createdAt.Time)
} else {
fmt.Println("创建时间: NULL")
}
}
常见错误 #
- 未关闭资源:如
rows.Close()、stmt.Close() - 忽略错误检查:尤其是
rows.Err() - 在循环中频繁准备语句:应在循环外准备,循环内执行
- 不正确的事务处理:没有处理提交或回滚
- 连接泄漏:比如使用了
Query但没有迭代完所有结果
最佳实践 #
- 始终使用参数化查询:防止SQL注入攻击
// 好的做法
db.Query("SELECT * FROM users WHERE name = ?", username)
// 不安全的做法
db.Query("SELECT * FROM users WHERE name = '" + username + "'")
- 正确处理连接:设置合适的连接池参数
- 使用合适的查询函数:
Query: 返回多行QueryRow: 返回单行Exec: 不返回行的操作(INSERT/UPDATE/DELETE)
- 使用事务确保数据完整性:在需要原子操作时使用事务
- 处理NULL值:使用
sql.NullXXX类型处理可能为NULL的列 - 结构化应用代码:将数据库操作封装到专门的包或函数中