数据库操作:Go 与 SQL 的完美邂逅

从零开始学习 Go 的 database/sql 包,掌握数据库连接、查询、事务处理等核心技能

数据库操作: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("数据库连接成功!")
}

几个重要的点:

  1. sql.Open 不会立即建立连接,它只是初始化一个 *sql.DB 对象。真正的连接是在第一次执行查询时建立的。
  2. db.Ping() 用于测试连接,它会立即尝试建立一个连接。
  3. defer db.Close() 确保程序结束时关闭连接池
  4. 导入驱动时用 _ 是因为我们只需要它的 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)
}

注意:

  1. 一定要 defer rows.Close(),否则会泄漏连接
  2. rows.Err() 检查迭代错误,因为 rows.Next() 可能因为错误而返回 false
  3. 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.NullStringsql.NullInt64sql.NullFloat64sql.NullBoolsql.NullTime 等类型都有两个字段:

  • Valid:布尔值,表示值是否有效(非 NULL)
  • 具体值:StringInt64Float64BoolTime

实战:用户管理系统

让我们把所学知识综合起来,实现一个简单的用户管理系统:

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 的数据库操作:

  1. 连接数据库sql.Open 和连接池配置
  2. CRUD 操作:增删改查的基本用法
  3. 查询技巧:单条、多条、Map 映射
  4. 预处理语句:提升性能和安全性
  5. 事务处理:确保数据一致性
  6. NULL 值处理:使用 sql.Null* 类型
  7. 实战应用:用户管理系统

database/sql 是 Go 标准库中的瑰宝,它简洁而强大。虽然市面上有很多 ORM 框架(如 GORM),但理解底层的 database/sql 能让你写出更高效、更可控的代码。

练习时间

  1. 创建一个博客系统,包含文章表和评论表,实现文章的增删改查和评论功能
  2. 实现一个分页查询函数,支持按字段排序
  3. 写一个数据迁移工具,把一个表的数据导入到另一个表
  4. 实现一个连接池监控工具,定期打印连接池状态

我们下篇见!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页