第一章:Go 数据库驱动与连接池
1.1 标准库 database/sql 的核心抽象
Go 通过 database/sql 提供统一接口,具体实现由驱动提供:
import (
"database/sql"
_ "github.com/lib/pq" // PostgreSQL 驱动(匿名导入注册)
)
db, err := sql.Open("postgres", "user=... password=... dbname=...")
关键点:
sql.Open()不建立连接,仅初始化连接池- 首次查询时才真正连接数据库
1.2 连接池配置(避免生产事故)
默认连接池无上限,高并发下易触发数据库 max_connections 限制。
关键参数
| 参数 | 默认值 | 建议值 | 说明 |
|---|
SetMaxOpenConns| 0(无限制) | 20--50 | 最大打开连接数SetMaxIdleConns| 2 | = MaxOpenConns | 最大空闲连接数SetConnMaxLifetime| 0(永不过期) | 30m | 连接最大存活时间
示例配置
// internal/db/db.go
func NewDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
// 验证连接
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
监控建议:
- 记录
db.Stats()(OpenConnections,InUse)- 设置告警:
InUse > MaxOpenConns * 0.8
第二章:ORM 选型 ------ GORM vs sqlx vs raw SQL
2.1 GORM:功能全面的现代化 ORM
-
优点 :
- 自动迁移(AutoMigrate)
- 关联加载(Has One/Many, Belongs To)
- 钩子(BeforeCreate, AfterFind)
- 软删除、批量操作、预加载
-
缺点 :
- 学习曲线陡峭
- 生成 SQL 不透明(需开启日志)
- 性能略低于手写 SQL
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
2.2 sqlx:轻量级增强版 database/sql
-
优点 :
- 结构体扫描(
StructScan) - 命名参数(
db.NamedExec) - 与标准库无缝兼容
- 结构体扫描(
-
缺点 :
- 无关联加载、无迁移工具
go get github.com/jmoiron/sqlx
2.3 raw SQL:极致控制与性能
- 适用场景 :
- 复杂 JOIN 查询
- 需要精确控制索引使用
- 高频读写路径(如计数器)
- 风险 :
- 易出错(拼写、注入)
- 难以复用
2.4 选型建议
| 场景 | 推荐 |
|---|
- 快速原型、CRUD 为主 | GORM
- 简单查询、已有 SQL 经验 | sqlx
- 复杂分析、高频交易 | raw SQL + sqlx 扫描
本篇选择 GORM :因其在开发效率 与功能完整性上最佳平衡。
第三章:Repository 模式 ------ 解耦业务与数据
3.1 为什么需要 Repository?
传统 MVC 中,Controller 直接调用 Model 方法,导致:
- 业务逻辑与 SQL 混杂
- 难以 mock 数据库进行单元测试
- 更换数据库需重写所有查询
Repository 模式通过接口隔离:
Handler → Service → Repository Interface → GORM Implementation
3.2 定义 Repository 接口
// internal/repository/user.go
type User struct {
ID string `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
Role string
}
type UserRepository interface {
Create(ctx context.Context, user *User) error
FindByID(ctx context.Context, id string) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
List(ctx context.Context, page, size int) ([]*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
关键原则:
- 方法命名体现业务意图(非 SQL 动词)
- 所有方法接收
context.Context(支持取消/超时)- 返回具体错误(如
ErrUserNotFound)
3.3 GORM 实现 Repository
// internal/repository/gorm/user.go
type gormUserRepo struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &gormUserRepo{db: db}
}
func (r *gormUserRepo) FindByID(ctx context.Context, id string) (*User, error) {
var user User
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
return &user, nil
}
func (r *gormUserRepo) List(ctx context.Context, page, size int) ([]*User, error) {
offset := (page - 1) * size
var users []*User
if err := r.db.WithContext(ctx).Offset(offset).Limit(size).Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
优势:
- 业务层只依赖
UserRepository接口- 测试时可替换为内存实现(见第7章)
第四章:实战 ------ 用户-订单-商品系统
4.1 数据模型设计
// User
type User struct {
ID string `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
Orders []Order `gorm:"foreignKey:UserID"` // 一对多
}
// Order
type Order struct {
ID string `gorm:"primaryKey"`
UserID string
User User `gorm:"foreignKey:UserID"` // 多对一
Items []OrderItem `gorm:"foreignKey:OrderID"`
Total float64
Status string `gorm:"default:'pending'"`
}
// OrderItem
type OrderItem struct {
ID string `gorm:"primaryKey"`
OrderID string
ProductID string
Product Product `gorm:"foreignKey:ProductID"`
Quantity int
Price float64
}
// Product
type Product struct {
ID string `gorm:"primaryKey"`
Name string
Price float64
Stock int
}
GORM 关联:
foreignKey指定外键字段- 预加载(Preload)自动加载关联数据
4.2 复杂查询实现
(1) 获取用户订单(含商品详情)
func (r *gormOrderRepo) GetOrderByIDWithItems(ctx context.Context, id string) (*Order, error) {
var order Order
err := r.db.WithContext(ctx).
Preload("Items.Product"). // 加载 OrderItem 及其 Product
First(&order, "id = ?", id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrOrderNotFound
}
return nil, err
}
return &order, nil
}
(2) 模糊搜索商品
func (r *gormProductRepo) Search(ctx context.Context, keyword string, page, size int) ([]*Product, error) {
offset := (page - 1) * size
var products []*Product
err := r.db.WithContext(ctx).
Where("name ILIKE ?", "%"+keyword+"%").
Offset(offset).Limit(size).
Find(&products).Error
return products, err
}
注意 :
ILIKE是 PostgreSQL 的大小写不敏感 LIKE。
第五章:事务管理 ------ 保证数据一致性
5.1 何时需要事务?
- 创建订单时:
- 扣减商品库存
- 创建订单记录
- 更新用户积分
任一失败,全部回滚
5.2 在 Repository 层封装事务
// internal/repository/transaction.go
type TxRepository interface {
UserRepository
OrderRepository
ProductRepository
}
func (r *gormRepo) WithTx(ctx context.Context, fn func(TxRepository) error) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txRepo := &gormRepo{
userRepo: &gormUserRepo{db: tx},
orderRepo: &gormOrderRepo{db: tx},
productRepo: &gormProductRepo{db: tx},
}
return fn(txRepo)
})
}
业务层使用
// internal/service/order.go
func (s *OrderService) CreateOrder(ctx context.Context, userID string, items []CartItem) error {
return s.repo.WithTx(ctx, func(tx TxRepository) error {
// 1. 检查库存
for _, item := range items {
product, err := tx.Product().FindByID(ctx, item.ProductID)
if err != nil {
return err
}
if product.Stock < item.Quantity {
return ErrInsufficientStock
}
// 2. 扣库存
product.Stock -= item.Quantity
if err := tx.Product().Update(ctx, product); err != nil {
return err
}
}
// 3. 创建订单
order := buildOrder(userID, items)
return tx.Order().Create(ctx, order)
})
}
关键 :所有操作通过
tx执行,共享同一事务。
第六章:数据库迁移 ------ 使用 Goose
6.1 为什么不用 GORM AutoMigrate?
- 生产环境禁止自动修改 schema
- 无法处理数据迁移(如字段拆分)
- 无版本控制、无回滚脚本
Goose 是专为 Go 设计的迁移工具:
go install github.com/pressly/goose/v3/cmd/goose@latest
6.2 初始化迁移
mkdir migrations
goose -dir migrations postgres "user=... dbname=..." create create_users_table sql
生成 migrations/20240501120000_create_users_table.sql:
-- +goose Up
CREATE TABLE users (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- +goose Down
DROP TABLE users;
6.3 执行迁移
# 应用所有未执行的迁移
goose -dir migrations postgres "dsn" up
# 回滚最后一次迁移
goose -dir migrations postgres "dsn" down
集成到启动流程:
// main.go
if err := goose.Up(db.DB(), "migrations"); err != nil {
log.Fatal("Migration failed:", err)
}
第七章:测试策略 ------ Mock Repository
7.1 为什么不能直接测数据库?
- 速度慢(100ms/测试 vs 1ms)
- 需要外部依赖(PostgreSQL)
- 测试间状态污染
解决方案:Mock Repository 接口
// internal/repository/mock/user.go (使用 testify/mock)
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
args := m.Called(ctx, id)
return args.Get(0).(*User), args.Error(1)
}
7.2 服务层单元测试
func TestOrderService_CreateOrder_InsufficientStock(t *testing.T) {
mockRepo := new(MockTxRepository)
service := NewOrderService(mockRepo)
// 模拟库存不足
mockRepo.On("Product().FindByID", mock.Anything, "prod-1").
Return(&Product{ID: "prod-1", Stock: 1}, nil)
items := []CartItem{{ProductID: "prod-1", Quantity: 2}}
err := service.CreateOrder(context.Background(), "user-1", items)
assert.Equal(t, ErrInsufficientStock, err)
mockRepo.AssertExpectations(t)
}
优势:
- 测试速度快
- 覆盖异常路径(如库存不足)
- 不依赖真实数据库
第八章:性能优化与监控
8.1 开启 GORM 日志
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // 打印所有 SQL
})
生产建议:仅记录慢查询(>100ms)
8.2 使用 EXPLAIN 分析查询
var users []User
db.Session(&gorm.Session{Logger: db.Logger}).Debug().
Where("email = ?", "alice@example.com").
Find(&users)
输出:
[0.521ms] [rows:1] SELECT * FROM "users" WHERE email = 'alice@example.com'
优化手段:
- 为
- 避免
SELECT *,只查必要字段
8.3 连接池监控
// 每分钟记录 stats
ticker := time.NewTicker(1 * time.Minute)
go func() {
for range ticker.C {
stats := db.Stats()
logrus.WithFields(logrus.Fields{
"open": stats.OpenConnections,
"in_use": stats.InUse,
"idle": stats.Idle,
}).Info("DB connection pool stats")
}
}()
结语:数据是系统的基石
一个健壮的数据层,不仅是 CRUD 的容器,更是业务规则、一致性、性能 的守护者。
通过本篇,你已具备构建生产级 Go 数据访问层的全部能力。