数据库操作:Go 与 SQL 的完美邂逅
几乎每一个真实的应用都需要和数据库打交道。无论是用户信息、订单数据还是日志记录,数据库都是我们存储和查询数据的核心工具。
Go 语言通过 database/sql 包提供了一个优雅而强大的数据库访问接口。它不直接实现任何数据库驱动,而是定义了一套标准接口,让各种数据库驱动(MySQL、PostgreSQL、SQLite 等)来实现具体的连接和操作。
今天我们就来学习如何用 Go 操作数据库,从基础的增删改查到高级的事务处理。
准备工作
在开始之前,你需要安装对应的数据库驱动。Go 社区为各种主流数据库都提供了驱动:
# MySQL
go get -u github.com/go-sql-driver/mysql
# PostgreSQL
go get -u github.com/lib/pq
# SQLite
go get -u github.com/mattn/go-sqlite3
本文以 MySQL 为例,但代码对其他数据库也适用(只需要改一下连接字符串)。
连接数据库
首先,我们需要打开一个数据库连接:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 连接字符串格式:用户名:密码@协议(地址)/数据库名?参数
dsn := "root:password@tcp(localhost:3306)/myapp?charset=utf8mb4&parseTime=True"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("打开数据库失败:", err)
}
defer db.Close()
// 测试连接是否成功
err = db.Ping()
if err != nil {
log.Fatal("连接数据库失败:", err)
}
fmt.Println("数据库连接成功!")
}
几个重要的点:
sql.Open不会立即建立连接,它只是初始化一个*sql.DB对象。真正的连接是在第一次执行查询时建立的。db.Ping()用于测试连接,它会立即尝试建立一个连接。defer db.Close()确保程序结束时关闭连接池。- 导入驱动时用
_是因为我们只需要它的init()函数来注册驱动,不需要直接使用它。
连接池配置
database/sql 自动管理一个连接池,你可以配置它的行为:
db.SetMaxOpenConns(25) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
合理的配置可以显著提升应用性能。一般来说:
MaxOpenConns设置为数据库允许的最大连接数(比如 MySQL 默认是 151)MaxIdleConns设置为MaxOpenConns的一半左右ConnMaxLifetime避免连接过久导致的问题
创建表
让我们先创建一个示例表:
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`
_, err := db.Exec(createTableSQL)
if err != nil {
log.Fatal("创建表失败:", err)
}
fmt.Println("表创建成功!")
db.Exec() 用于执行不返回结果集的 SQL 语句(CREATE、INSERT、UPDATE、DELETE 等)。
插入数据
单条插入
insertSQL := "INSERT INTO users (name, email, age) VALUES (?, ?, ?)"
result, err := db.Exec(insertSQL, "张三", "zhangsan@example.com", 25)
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)
注意 Go 的 ? 占位符会自动处理 SQL 注入问题,这比字符串拼接安全得多。
批量插入
users := []struct {
Name string
Email string
Age int
}{
{"李四", "lisi@example.com", 30},
{"王五", "wangwu@example.com", 28},
{"赵六", "zhaoliu@example.com", 35},
}
// 开启事务批量插入
tx, err := db.Begin()
if err != nil {
log.Fatal("开启事务失败:", err)
}
stmt, err := tx.Prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
tx.Rollback()
log.Fatal("预处理失败:", err)
}
defer stmt.Close()
for _, u := range users {
_, err := stmt.Exec(u.Name, u.Email, u.Age)
if err != nil {
tx.Rollback()
log.Printf("插入 %s 失败: %v", u.Name, err)
}
}
err = tx.Commit()
if err != nil {
log.Fatal("提交事务失败:", err)
}
fmt.Println("批量插入成功!")
使用事务和预处理语句可以显著提升批量插入的性能。
查询数据
查询单条记录
var id int
var name, email string
var age int
var createdAt time.Time
querySQL := "SELECT id, name, email, age, created_at FROM users WHERE id = ?"
err := db.QueryRow(querySQL, 1).Scan(&id, &name, &email, &age, &createdAt)
switch {
case err == sql.ErrNoRows:
fmt.Println("用户不存在")
case err != nil:
log.Fatal("查询失败:", err)
default:
fmt.Printf("用户: %s (%s), 年龄: %d, 创建时间: %v\n",
name, email, age, createdAt)
}
db.QueryRow() 用于查询单条记录,它比 db.Query() 更高效。Scan() 会把结果映射到你提供的变量中。
查询多条记录
querySQL := "SELECT id, name, email, age FROM users WHERE age > ?"
rows, err := db.Query(querySQL, 25)
if err != nil {
log.Fatal("查询失败:", err)
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if err != nil {
log.Fatal("扫描失败:", err)
}
users = append(users, u)
}
// 检查迭代过程中是否有错误
err = rows.Err()
if err != nil {
log.Fatal("迭代失败:", err)
}
fmt.Printf("找到 %d 个用户:\n", len(users))
for _, u := range users {
fmt.Printf("- %s (%s), 年龄: %d\n", u.Name, u.Email, u.Age)
}
注意:
- 一定要
defer rows.Close(),否则会泄漏连接 - 用
rows.Err()检查迭代错误,因为rows.Next()可能因为错误而返回 false Scan的顺序必须和SELECT的字段顺序一致
查询到 Map
如果你不想定义结构体,可以查询到 map[string]interface{}:
rows, _ := db.Query("SELECT * FROM users")
columns, _ := rows.Columns()
for rows.Next() {
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range values {
valuePtrs[i] = &values[i]
}
rows.Scan(valuePtrs...)
row := make(map[string]interface{})
for i, col := range columns {
row[col] = values[i]
}
fmt.Println(row)
}
更新和删除
// 更新
updateSQL := "UPDATE users SET age = ? WHERE id = ?"
result, err := db.Exec(updateSQL, 26, 1)
if err != nil {
log.Fatal("更新失败:", err)
}
rows, _ := result.RowsAffected()
fmt.Printf("更新了 %d 行\n", rows)
// 删除
deleteSQL := "DELETE FROM users WHERE id = ?"
result, err = db.Exec(deleteSQL, 1)
if err != nil {
log.Fatal("删除失败:", err)
}
rows, _ = result.RowsAffected()
fmt.Printf("删除了 %d 行\n", rows)
预处理语句
预处理语句可以提升性能(数据库可以缓存执行计划)并防止 SQL 注入:
stmt, err := db.Prepare("SELECT name, email FROM users WHERE age > ?")
if err != nil {
log.Fatal("预处理失败:", err)
}
defer stmt.Close()
// 多次执行
rows1, _ := stmt.Query(25)
// 处理结果...
rows2, _ := stmt.Query(30)
// 处理结果...
事务处理
事务确保一组操作要么全部成功,要么全部失败:
func transferMoney(db *sql.DB, fromID, toID int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
// 从 fromID 扣款
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
if err != nil {
tx.Rollback()
return err
}
// 向 toID 加款
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
err := transferMoney(db, 1, 2, 100.00)
if err != nil {
log.Fatal("转账失败:", err)
}
fmt.Println("转账成功!")
带重试的事务
网络抖动可能导致事务失败,我们可以加重试逻辑:
func withRetry(db *sql.DB, fn func(tx *sql.Tx) error, maxRetries int) error {
for i := 0; i < maxRetries; i++ {
tx, err := db.Begin()
if err != nil {
return err
}
err = fn(tx)
if err != nil {
tx.Rollback()
// 如果是死锁错误,重试
if isDeadlockError(err) {
time.Sleep(time.Duration(i*100) * time.Millisecond)
continue
}
return err
}
err = tx.Commit()
if err == nil {
return nil
}
}
return fmt.Errorf("达到最大重试次数")
}
func isDeadlockError(err error) bool {
// 根据数据库类型判断死锁错误
return strings.Contains(err.Error(), "Deadlock")
}
NULL 值处理
数据库中的 NULL 值在 Go 中需要特殊处理:
import "database/sql"
type User struct {
ID int
Name string
Email sql.NullString // 可能为 NULL
Age sql.NullInt64 // 可能为 NULL
}
var u User
err := db.QueryRow("SELECT id, name, email, age FROM users WHERE id = ?", 1).
Scan(&u.ID, &u.Name, &u.Email, &u.Age)
if u.Email.Valid {
fmt.Println("邮箱:", u.Email.String)
} else {
fmt.Println("邮箱: NULL")
}
if u.Age.Valid {
fmt.Println("年龄:", u.Age.Int64)
} else {
fmt.Println("年龄: NULL")
}
sql.NullString、sql.NullInt64、sql.NullFloat64、sql.NullBool、sql.NullTime 等类型都有两个字段:
Valid:布尔值,表示值是否有效(非 NULL)- 具体值:
String、Int64、Float64、Bool、Time
实战:用户管理系统
让我们把所学知识综合起来,实现一个简单的用户管理系统:
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID int
Name string
Email string
Age int
CreatedAt time.Time
}
type UserManager struct {
db *sql.DB
}
func NewUserManager(db *sql.DB) *UserManager {
return &UserManager{db: db}
}
func (m *UserManager) Create(name, email string, age int) (int, error) {
result, err := m.db.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
name, email, age,
)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
return int(id), err
}
func (m *UserManager) GetByID(id int) (*User, error) {
var u User
err := m.db.QueryRow(
"SELECT id, name, email, age, created_at FROM users WHERE id = ?", id,
).Scan(&u.ID, &u.Name, &u.Email, &u.Age, &u.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &u, nil
}
func (m *UserManager) List(minAge int) ([]User, error) {
rows, err := m.db.Query(
"SELECT id, name, email, age, created_at FROM users WHERE age >= ? ORDER BY id",
minAge,
)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &u.CreatedAt)
if err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
func (m *UserManager) Update(id int, name, email string, age int) error {
_, err := m.db.Exec(
"UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?",
name, email, age, id,
)
return err
}
func (m *UserManager) Delete(id int) error {
_, err := m.db.Exec("DELETE FROM users WHERE id = ?", id)
return err
}
func main() {
db, err := sql.Open("mysql", "root:password@tcp(localhost:3306)/myapp")
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
manager := NewUserManager(db)
// 创建用户
id, err := manager.Create("张三", "zhangsan@example.com", 25)
if err != nil {
log.Fatal("创建失败:", err)
}
fmt.Println("创建用户 ID:", id)
// 查询用户
user, err := manager.GetByID(id)
if err != nil {
log.Fatal("查询失败:", err)
}
fmt.Printf("用户: %+v\n", user)
// 更新用户
err = manager.Update(id, "张三(已更新)", "zhangsan_new@example.com", 26)
if err != nil {
log.Fatal("更新失败:", err)
}
// 列出用户
users, err := manager.List(20)
if err != nil {
log.Fatal("列表失败:", err)
}
fmt.Printf("找到 %d 个用户\n", len(users))
// 删除用户
err = manager.Delete(id)
if err != nil {
log.Fatal("删除失败:", err)
}
fmt.Println("删除成功")
}
小结
今天我们学习了 Go 的数据库操作:
- 连接数据库:
sql.Open和连接池配置 - CRUD 操作:增删改查的基本用法
- 查询技巧:单条、多条、Map 映射
- 预处理语句:提升性能和安全性
- 事务处理:确保数据一致性
- NULL 值处理:使用
sql.Null*类型 - 实战应用:用户管理系统
database/sql 是 Go 标准库中的瑰宝,它简洁而强大。虽然市面上有很多 ORM 框架(如 GORM),但理解底层的 database/sql 能让你写出更高效、更可控的代码。
练习时间
- 创建一个博客系统,包含文章表和评论表,实现文章的增删改查和评论功能
- 实现一个分页查询函数,支持按字段排序
- 写一个数据迁移工具,把一个表的数据导入到另一个表
- 实现一个连接池监控工具,定期打印连接池状态
我们下篇见!
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。