第一章: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/postgres2.2 sqlx:轻量级增强版 database/sql
- 优点:
- 结构体扫描(
StructScan) - 命名参数(
db.NamedExec) - 与标准库无缝兼容
- 结构体扫描(
- 缺点:
- 无关联加载、无迁移工具
go get github.com/jmoiron/sqlx2.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 Implementation3.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@latest6.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 数据访问层的全部能力。