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)
}
github.com/go-sql-driver/mysql
github.com/godror/godror
github.com/lib/pq
go.mongodb.org/mongo-driver/mongo
modernc.org/sqlite
github.com/mattn/go-sqlite3
# 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)
}
预处理语句是指在数据库中提前编译和优化的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值需要使用特殊的类型处理:
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
但没有迭代完所有结果// 好的做法
db.Query("SELECT * FROM users WHERE name = ?", username)
// 不安全的做法
db.Query("SELECT * FROM users WHERE name = '" + username + "'")
Query
: 返回多行QueryRow
: 返回单行Exec
: 不返回行的操作(INSERT/UPDATE/DELETE)sql.NullXXX
类型处理可能为NULL的列