Go 后端开发学习指南

文章目录

  • 前言
    • [1. Go 语言快速入门](#1. Go 语言快速入门)
      • [1.1 什么是 Go?](#1.1 什么是 Go?)
      • [1.2 包管理(go.mod)](#1.2 包管理(go.mod))
      • [1.3 项目内部包的导入路径](#1.3 项目内部包的导入路径)
      • [1.4 Go 关键语法速查](#1.4 Go 关键语法速查)
    • [2. 项目工程结构](#2. 项目工程结构)
    • [3. Gin Web 框架 --- HTTP 服务](#3. Gin Web 框架 — HTTP 服务)
      • [3.1 Gin 是什么?](#3.1 Gin 是什么?)
      • [3.2 创建 Gin 引擎](#3.2 创建 Gin 引擎)
      • [3.3 路由注册](#3.3 路由注册)
      • [3.4 Gin Context --- 一次请求的完整生命周期](#3.4 Gin Context — 一次请求的完整生命周期)
      • [3.5 程序启动流程](#3.5 程序启动流程)
      • [3.6 优雅关闭](#3.6 优雅关闭)
    • [4. MySQL 数据库连接与 GORM ORM](#4. MySQL 数据库连接与 GORM ORM)
      • [4.1 ORM 概念](#4.1 ORM 概念)
      • [4.2 GORM 初始化](#4.2 GORM 初始化)
      • [4.3 连接池配置](#4.3 连接池配置)
      • [4.4 模型定义(GORM Model)](#4.4 模型定义(GORM Model))
      • [4.5 软删除(Soft Delete)](#4.5 软删除(Soft Delete))
      • [4.6 数据库迁移(AutoMigrate)](#4.6 数据库迁移(AutoMigrate))
    • [5. Redis 缓存与连接管理](#5. Redis 缓存与连接管理)
      • [5.1 Redis 是什么?](#5.1 Redis 是什么?)
      • [5.2 单机模式 vs 集群模式](#5.2 单机模式 vs 集群模式)
      • [5.3 统一接口 --- 屏蔽差异](#5.3 统一接口 — 屏蔽差异)
      • [5.4 连接池配置](#5.4 连接池配置)
      • [5.5 自动重试机制](#5.5 自动重试机制)
      • [5.6 缓存抽象层](#5.6 缓存抽象层)
    • [6. 增删改查 CRUD 完整实战](#6. 增删改查 CRUD 完整实战)
      • [6.1 创建商品(POST /admin/v1/products)](#6.1 创建商品(POST /admin/v1/products))
      • [6.2 分页查询商品(GET /admin/v1/products?page=1&size=10)](#6.2 分页查询商品(GET /admin/v1/products?page=1&size=10))
      • [6.3 部分更新商品(PUT /admin/v1/products/:id)](#6.3 部分更新商品(PUT /admin/v1/products/:id))
      • [6.4 删除商品(DELETE /admin/v1/products/:id)](#6.4 删除商品(DELETE /admin/v1/products/:id))
    • [7. 三层架构:Handler → Service → Repository](#7. 三层架构:Handler → Service → Repository)
      • [7.1 架构图](#7.1 架构图)
      • [7.2 每层依赖的是接口,不是实现](#7.2 每层依赖的是接口,不是实现)
      • [7.3 依赖注入容器](#7.3 依赖注入容器)
      • [7.4 分层的核心价值](#7.4 分层的核心价值)
    • [8. JWT 用户认证与授权](#8. JWT 用户认证与授权)
      • [8.1 JWT 是什么?](#8.1 JWT 是什么?)
      • [8.2 登录流程](#8.2 登录流程)
      • [8.3 Token 生成](#8.3 Token 生成)
      • [8.4 JWT 认证中间件](#8.4 JWT 认证中间件)
    • [9. 中间件系统](#9. 中间件系统)
      • [9.1 什么是中间件?](#9.1 什么是中间件?)
      • [9.2 中间件注册顺序](#9.2 中间件注册顺序)
      • [9.3 Recovery --- Panic 恢复](#9.3 Recovery — Panic 恢复)
      • [9.4 CORS --- 跨域资源共享](#9.4 CORS — 跨域资源共享)
      • [9.5 安全 HTTP 响应头](#9.5 安全 HTTP 响应头)
      • [9.6 RequestID --- 请求追踪](#9.6 RequestID — 请求追踪)
    • [10. 错误处理与错误码设计](#10. 错误处理与错误码设计)
      • [10.1 错误分类](#10.1 错误分类)
      • [10.2 错误码表](#10.2 错误码表)
      • [10.3 AppError 不可变模式](#10.3 AppError 不可变模式)
      • [10.4 统一错误响应](#10.4 统一错误响应)
    • [11. 配置管理与多环境](#11. 配置管理与多环境)
      • [11.1 两层配置覆盖](#11.1 两层配置覆盖)
      • [11.2 支持的配置项](#11.2 支持的配置项)
    • [12. 结构化日志与请求追踪](#12. 结构化日志与请求追踪)
      • [12.1 Zap 日志库](#12.1 Zap 日志库)
      • [12.2 全链路追踪](#12.2 全链路追踪)
      • [12.3 日志滚动](#12.3 日志滚动)
    • [13. 缓存策略与缓存穿透防护](#13. 缓存策略与缓存穿透防护)
      • [13.1 Cache Aside(旁路缓存)模式](#13.1 Cache Aside(旁路缓存)模式)
      • [13.2 缓存穿透防护](#13.2 缓存穿透防护)
      • [13.3 缓存 Key 命名规范](#13.3 缓存 Key 命名规范)
    • [14. 限流器 Rate Limiter](#14. 限流器 Rate Limiter)
      • [14.1 令牌桶算法](#14.1 令牌桶算法)
      • [14.2 使用方式](#14.2 使用方式)
      • [14.3 限流响应](#14.3 限流响应)
    • [15. Prometheus 监控指标](#15. Prometheus 监控指标)
      • [15.1 指标类型](#15.1 指标类型)
      • [15.2 监控的维度](#15.2 监控的维度)
      • [15.3 连接池监控](#15.3 连接池监控)
    • [16. 消息队列 MQ](#16. 消息队列 MQ)
      • [16.1 概念](#16.1 概念)
      • [16.2 双驱动支持](#16.2 双驱动支持)
      • [16.3 配置](#16.3 配置)
    • [17. 定时任务 Scheduler](#17. 定时任务 Scheduler)
      • [17.1 Cron 表达式](#17.1 Cron 表达式)
      • [17.2 默认任务](#17.2 默认任务)
    • [18. 安全最佳实践](#18. 安全最佳实践)
      • [18.1 密码安全](#18.1 密码安全)
      • [18.2 SQL 注入防护](#18.2 SQL 注入防护)
      • [18.3 反时序攻击(防用户枚举)](#18.3 反时序攻击(防用户枚举))
      • [18.4 密码不出现在 JSON 响应中](#18.4 密码不出现在 JSON 响应中)
    • [19. 测试体系](#19. 测试体系)
      • [19.1 测试类型](#19.1 测试类型)
      • [19.2 Repository 层测试(sqlmock)](#19.2 Repository 层测试(sqlmock))
      • [19.3 Handler 层测试(httptest)](#19.3 Handler 层测试(httptest))
      • [19.4 运行测试命令](#19.4 运行测试命令)

前言

  如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。

  而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!!


1. Go 语言快速入门

参考项目:

地址:https://github.com/yangzhenyu07/yzyTool

下图红框处。

1.1 什么是 Go?

Go(又称 Golang)是 Google 开发的静态类型、编译型编程语言,天然支持并发(goroutine),特别适合编写高性能后端服务。

核心特点:

  • 编译型:代码直接编译为机器码,运行极快
  • 静态类型:变量类型在编译时确定,减少运行时错误
  • 内置并发:goroutine(轻量级线程)+ channel(通道)天然支持高并发
  • 垃圾回收:自动管理内存,不需要手动释放
  • 简洁语法:只有 25 个关键字,学习曲线平缓

1.2 包管理(go.mod)

Go 的依赖管理通过 go.mod 文件来定义,类似于 Node.js 的 package.json 或 Java 的 pom.xml

go 复制代码
// go.mod 示例(来自本项目)
module yangzhenyu.com/go-yzy  // ← 模块名,导入其他包时使用这个前缀

go 1.25.0  // ← Go 版本要求

require (
    github.com/gin-gonic/gin v1.12.0    // ← 直接依赖
    gorm.io/gorm v1.31.1                 // ← 直接依赖
)

关键概念:

  • go mod init --- 初始化新模块
  • go mod tidy --- 自动整理依赖(清理用不到的、下载缺失的)
  • go mod download --- 下载所有依赖到本地缓存
  • go get 包名 --- 添加新依赖
  • 间接依赖(indirect)--- 你依赖的库所依赖的库,通常不需要手动管理

1.3 项目内部包的导入路径

在本项目中,所有内部包都以 yangzhenyu.com/go-yzy/ 开头导入:

go 复制代码
import (
    "yangzhenyu.com/go-yzy/internal/model"      // 导入 model 包
    "yangzhenyu.com/go-yzy/internal/service"     // 导入 service 包
    "yangzhenyu.com/go-yzy/internal/database"     // 导入 database 包
)

1.4 Go 关键语法速查

go 复制代码
// ── 变量声明 ──
var name string = "张三"     // 完整声明
name := "张三"               // 短声明(自动推断类型,仅在函数内可用)

// ── 常量 ──
const maxRetry = 3           // 编译时确定的值

// ── 结构体(类似其他语言的 class 数据部分) ──
type User struct {
    Name string  // 大写开头 = 公开(exported),可被其他包访问
    age  int     // 小写开头 = 私有(unexported),仅本包可访问
}

// ── 方法(给结构体绑定函数) ──
func (u *User) GetName() string {  // (u *User) 是"接收者",类似 Java 的 this
    return u.Name
}

// ── 接口(定义行为,隐式实现) ──
type Reader interface {
    Read(p []byte) (n int, err error)
}
// 任何有 Read([]byte) (int, error) 方法的类型都自动实现了 Reader 接口

// ── 错误处理(Go 没有 try-catch) ──
result, err := someFunction()
if err != nil {
    return err  // 错误必须显式检查
}

// ── defer(延迟执行,常用于关闭资源) ──
func readFile() error {
    f, err := os.Open("file.txt")
    if err != nil { return err }
    defer f.Close()  // 函数返回前一定会执行 Close()
    // ... 处理文件
}

// ── goroutine(并发) ──
go func() {
    // 这个函数会在新的 goroutine 中并发执行
    // goroutine 是 Go 的轻量级线程,栈只有几 KB
    // 一个 Go 程序可以轻松运行数十万个 goroutine
}()

// ── channel(goroutine 间通信) ──
ch := make(chan string)     // 创建通道
go func() { ch <- "hello" }() // 发送数据
msg := <-ch                  // 接收数据(阻塞等待)

// ── 错误包装(Go 1.13+) ──
originalErr := errors.New("database connection failed")
wrappedErr := fmt.Errorf("init failed: %w", originalErr)
// %w 会把原始错误包裹进去,后续可以用 errors.Is() 检查

2. 项目工程结构

Gonio 项目采用 Go 社区推荐的标准项目布局

复制代码
go-yzy/
├── cmd/                    # 程序入口
│   └── server/
│       ├── main.go         # 主函数,程序启动点
│       └── app.go          # App 结构体,管理 HTTP/MQ/Scheduler 生命周期
├── config/                 # 配置文件
│   ├── config.yaml         # 基础配置(所有环境共用)
│   ├── config_dev.yaml     # 开发环境覆盖配置
│   └── config_integration.yaml # 集成测试配置
├── internal/               # 内部代码(不可被外部项目导入)
│   ├── config/             # 配置加载与校验
│   ├── database/           # MySQL/Redis 初始化与连接管理
│   ├── handler/            # HTTP Handler 层(控制器)
│   ├── middleware/         # Gin 中间件
│   ├── model/              # 数据模型(ORM 映射)
│   ├── mq/                 # 消息队列
│   ├── pkg/                # 通用工具包
│   │   ├── auth/           # JWT 认证
│   │   ├── cache/          # Redis 缓存抽象
│   │   ├── errcode/        # 错误码系统
│   │   ├── i18n/           # 国际化/多语言
│   │   ├── logger/         # 结构化日志
│   │   ├── ratelimit/      # 限流器
│   │   ├── req/            # 请求体定义(DTO)
│   │   ├── response/       # 响应格式定义
│   │   └── validator/      # 数据验证器
│   ├── repository/         # 数据访问层(Repository 模式)
│   ├── router/             # 路由注册
│   ├── scheduler/          # 定时任务调度器
│   ├── service/            # 业务逻辑层
│   └── svc/                # 依赖注入容器(ServiceContext)
├── migration/              # 数据库迁移(建表)
├── test/                   # 测试代码
│   ├── *.go                # 单元测试(Mock)
│   └── integration/        # 集成测试(真实数据库)
├── docs/                   # Swagger 文档(自动生成)
├── learn/                  # 学习示例代码
├── examples/               # 使用示例
├── Dockerfile              # Docker 镜像构建
├── docker-compose.yml      # 本地开发环境编排
├── Makefile                # 常用命令快捷方式
└── go.mod                  # Go 模块定义(依赖管理)

设计原则:

  • cmd/ --- 每个子目录是一个可执行程序,只放 main.go,不放业务逻辑
  • internal/ --- Go 编译器禁止外部项目导入 internal 下的包,保护内部实现
  • pkg/ --- 可以被其他项目复用的通用工具(本项目中在 internal/pkg/ 下,不对外暴露)
  • test/ --- 独立测试目录,不与源代码混在一起

3. Gin Web 框架 --- HTTP 服务

3.1 Gin 是什么?

Gin 是 Go 语言最流行的 HTTP Web 框架(类似于 Node.js 的 Express、Python 的 Flask、Java 的 Spring Boot)。它速度快、功能全、生态丰富。

go 复制代码
// 导入 Gin
import "github.com/gin-gonic/gin"

3.2 创建 Gin 引擎

go 复制代码
// engine 是 Gin 的核心对象,管理所有路由和中间件
engine := gin.Default()    // 默认引擎(自带 Logger 和 Recovery 中间件)
gin.SetMode(gin.TestMode)   // 设置运行模式:debug / release / test

3.3 路由注册

Gin 支持 RESTful API 标准 HTTP 方法:

go 复制代码
// 来自 router/app/router.go
r := engine.Group("/app/v1")  // 路由组,所有路由以 /app/v1 开头
r.GET("/health", handler.Health)           // GET 请求
r.POST("/login", h.Login)                  // POST 请求
r.GET("/products/:id", productH.Get)       // 带路径参数(:id)
r.PUT("/products/:id", productH.Update)    // PUT 请求
r.DELETE("/products/:id", productH.Delete) // DELETE 请求

路由组(Group) 的作用:

  • 给一组路由加上统一的前缀
  • 可以对整个组应用中间件(如 JWT 认证)

3.4 Gin Context --- 一次请求的完整生命周期

*gin.Context 是 Gin 最重要的类型,它承载了一次 HTTP 请求的所有信息:

go 复制代码
func (h *ProductHandler) Get(c *gin.Context) {
    id := c.Param("id")              // 获取路径参数 /products/123 → "123"
    page := c.Query("page")          // 获取查询参数 ?page=1 → "1"
    
    var body SomeStruct
    c.ShouldBindJSON(&body)          // 绑定 JSON 请求体并校验
    
    c.JSON(200, gin.H{"msg": "ok"})  // 返回 JSON 响应
    c.Request.Context()              // 获取 Go 标准 Context(用于超时控制)
}

3.5 程序启动流程

Gonio 的启动流程非常规范,在 cmd/server/main.gorun() 函数中按顺序初始化:

复制代码
1. 加载配置 (config.Load)
2. 初始化日志 (logger.Init)
3. 设置 Gin 模式
4. 初始化 MySQL (database.InitMySQL)
5. 初始化 Redis (database.InitRedis)
6. 数据库自动迁移 (migration.AutoMigrate)
7. 初始化 JWT 中间件
8. 初始化验证器 (validator.Init)
9. 初始化国际化 (i18n.Init)
10. 初始化消息队列 MQ
11. 初始化 ServiceContext(依赖注入)
12. 设置路由 (router.Setup)
13. 初始化定时任务 (scheduler)
14. 启动 HTTP Server + MQ Router + Scheduler
15. 监听系统信号 → 优雅关闭

3.6 优雅关闭

优雅关闭(Graceful Shutdown)确保在服务停止时:

  1. 不再接受新请求
  2. 等待已在处理的请求完成
  3. 关闭 MQ 消费者
  4. 等待定时任务完成
  5. 关闭数据库连接
go 复制代码
// 监听 SIGINT(Ctrl+C)/ SIGTERM / SIGQUIT 信号
runCtx, stop := signal.NotifyContext(context.Background(), 
    syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer stop()

// 当收到信号时,ctx.Done() 被触发
select {
case <-ctx.Done():     // 收到关闭信号
case err := <-errCh:   // 某个组件出错
}

// 关闭顺序:HTTP → MQ → Scheduler(先停入口,再停消费者,最后停定时任务)
httpServer.Shutdown(shutdownCtx)
mqRouter.Close()
scheduler.Stop()

4. MySQL 数据库连接与 GORM ORM

4.1 ORM 概念

ORM(Object-Relational Mapping,对象关系映射)让你用 Go 的结构体来操作数据库表,而不需要手写 SQL:

复制代码
Go struct    ←→  GORM  ←→  MySQL 表
User{...}    ←→  ORM   ←→  users 表

4.2 GORM 初始化

go 复制代码
// 来自 internal/database/mysql.go

import (
    "gorm.io/driver/mysql"   // GORM 的 MySQL 驱动
    "gorm.io/gorm"           // GORM 核心
)

// 构造 DSN(Data Source Name,数据源名称)
// 格式:用户名:密码@tcp(主机:端口)/数据库名?参数1&参数2...
dsn := "root:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=UTC"

// 打开数据库连接
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    PrepareStmt:            true,  // 缓存预编译语句,减少 SQL 解析开销
    SkipDefaultTransaction: true,  // 跳过默认事务,提升 ~30% 性能
})

4.3 连接池配置

数据库连接是昂贵的资源(需要 TCP 三次握手、TLS 握手、认证),使用连接池可以复用连接:

go 复制代码
sqlDB, _ := db.DB()   // 从 *gorm.DB 获取底层 *sql.DB

sqlDB.SetMaxIdleConns(10)        // 最大空闲连接数
sqlDB.SetMaxOpenConns(100)       // 最大打开连接数
sqlDB.SetConnMaxLifetime(3600)   // 连接最大存活时间(秒)
sqlDB.SetConnMaxIdleTime(600)    // 空闲连接最大存活时间(秒)

连接池工作原理:

复制代码
请求到达 → 从池中获取连接 → 执行 SQL → 归还连接
                 ↑
           连接不够 → 创建新连接(不超过 MaxOpenConns)
           连接空闲超时 → 关闭连接(不低于 MaxIdleConns)

4.4 模型定义(GORM Model)

go 复制代码
// 来自 internal/model/user.go

type User struct {
    // 嵌入 BaseModel,复用公共字段
    // BaseModel 包含 ID、CreatedAt、UpdatedAt、DeletedAt
    BaseModel
    
    // struct tag 告诉 GORM 如何处理这个字段
    Username string `json:"username" gorm:"type:varchar(64);uniqueIndex;not null"`
    Password string `json:"-" gorm:"type:varchar(255);not null"`
    Status   int    `json:"status" gorm:"default:1;comment:1-正常 0-禁用"`
}

GORM 命名约定(约定优于配置):

  • 表名:结构体名的蛇形复数 → Userusers
  • 列名:字段名的蛇形 → CreatedAtcreated_at
  • 主键:名为 ID 的字段自动成为主键

struct tag 详解:

Tag 含义
json:"username" JSON 序列化时字段名为 "username"
json:"-" JSON 序列化时忽略该字段(常用于密码)
gorm:"primaryKey" 该字段是主键
gorm:"type:varchar(64)" 列类型为 VARCHAR(64)
gorm:"uniqueIndex" 创建唯一索引
gorm:"not null" 列不允许为空
gorm:"default:1" 默认值为 1
gorm:"index" 创建普通索引
gorm:"comment:说明" 数据库列注释

4.5 软删除(Soft Delete)

GORM 的软删除不是真的删除数据,而是设置 deleted_at 时间戳:

go 复制代码
// BaseModel 中的 DeletedAt 字段
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`

// 执行"删除"时
db.Delete(&user)  // 实际执行:UPDATE users SET deleted_at = NOW() WHERE id = ?
// 数据还在数据库中,但后续查询会自动排除 deleted_at IS NOT NULL 的记录

为什么用软删除?

  • 数据安全:误删后可以恢复
  • 审计需求:知道数据什么时候被标记为删除
  • 数据分析:历史数据仍然可以被统计

4.6 数据库迁移(AutoMigrate)

GORM 可以自动根据 Go 结构体创建/更新数据库表:

go 复制代码
// 来自 migration/migration.go
func AutoMigrate(db *gorm.DB) error {
    return db.AutoMigrate(
        &model.User{},
        &model.Admin{},
        &model.Product{},
        &model.Category{},
    )
}

AutoMigrate 会:

  • 创建不存在的表
  • 添加不存在的列
  • 创建不存在的索引
  • 不会删除已存在的列(安全设计,防止数据丢失)

5. Redis 缓存与连接管理

5.1 Redis 是什么?

Redis 是一个高性能的内存数据库,数据存在内存中(也支持持久化到磁盘),读写速度极快(微秒级),通常用于:

  • 缓存:存储热点数据,减轻数据库压力
  • 限流:统计请求频率
  • 消息队列:Redis Streams 支持发布订阅
  • 分布式锁:协调多个服务

5.2 单机模式 vs 集群模式

Gonio 同时支持两种 Redis 部署模式:

单机模式(Standalone):一个 Redis 实例

yaml 复制代码
redis:
  mode: standalone
  addr: "127.0.0.1:6379"
  db: 0    # 可选用第几个数据库(Redis 默认有 16 个逻辑数据库,0-15)

集群模式(Cluster):多个 Redis 节点组成集群

yaml 复制代码
redis:
  mode: cluster
  addrs:
    - "10.0.0.1:6379"  # 数据自动分片到不同节点
    - "10.0.0.2:6379"
    - "10.0.0.3:6379"
  # 集群模式下 db 固定为 0,不支持多数据库

5.3 统一接口 --- 屏蔽差异

这是本项目的重要设计 :定义 RedisClient 接口,让上层代码无需区分单机还是集群:

go 复制代码
// 来自 internal/database/redis_client.go

// RedisClient 统一 Redis 客户端接口
type RedisClient interface {
    redis.Scripter  // 嵌入接口,支持 Lua 脚本
    
    Get(ctx context.Context, key string) *redis.StringCmd    // 获取值
    Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd  // 设置值(带过期)
    Del(ctx context.Context, keys ...string) *redis.IntCmd   // 删除键
    Exists(ctx context.Context, keys ...string) *redis.IntCmd // 检查键是否存在
    Ping(ctx context.Context) *redis.StatusCmd                // 连通性检查
    Close() error                                             // 关闭连接
    PoolStats() *redis.PoolStats                              // 连接池统计
    // ... 更多方法
}

// 编译时验证:确保 *redis.Client 和 *redis.ClusterClient 都满足接口
var _ RedisClient = (*redis.Client)(nil)
var _ RedisClient = (*redis.ClusterClient)(nil)

5.4 连接池配置

go 复制代码
// Redis 连接池参数
PoolSize:     100,    // 连接池大小(总连接数)
MinIdleConns: 10,     // 最小空闲连接数(预先建立,避免冷启动延迟)
MaxIdleConns: 50,     // 最大空闲连接数
PoolTimeout:  6s,     // 获取连接超时时间
DialTimeout:  5s,     // 建立连接超时
ReadTimeout:  3s,     // 读取超时
WriteTimeout: 3s,     // 写入超时

5.5 自动重试机制

go 复制代码
// Redis 内置指数退避重试
MaxRetries:     3,                // 最多重试 3 次
MinRetryBackoff: 8 * time.Millisecond,   // 第 1 次重试等 8ms
MaxRetryBackoff: 512 * time.Millisecond, // 最大等待 512ms
// 实际等待时间:8ms → 16ms → 32ms → 64ms → ... (翻倍,不超过 512ms)

5.6 缓存抽象层

go 复制代码
// 来自 internal/pkg/cache/cache.go

// Cache 缓存接口,只暴露 Set/Get/Del 三个方法
type Cache interface {
    Set(ctx context.Context, key string, value string, expiration time.Duration) error
    Get(ctx context.Context, key string) (string, error)
    Del(ctx context.Context, key string) error
}

// ErrCacheMiss 缓存未命中哨兵错误
var ErrCacheMiss = errors.New("cache: key not found")

哨兵错误(Sentinel Error)模式 :用预定义的错误变量表示特定状态,调用方可以用 errors.Is() 精确判断。Service 层不直接依赖 redis.Nil,而是判断 cache.ErrCacheMiss,解耦了 Redis 依赖。


6. 增删改查 CRUD 完整实战

从一次完整的 HTTP 请求追踪整个调用链路,理解每一层做了什么。

6.1 创建商品(POST /admin/v1/products)

第 1 步 --- HTTP 请求到达

复制代码
POST /admin/v1/products
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
    "name": "测试商品",
    "price": 99.99,
    "stock": 100
}

第 2 步 --- Gin 路由匹配 → 经过认证中间件 → 调用 ProductHandler.Create(c)

第 3 步 --- Handler 层internal/handler/product_handler.go):

go 复制代码
func (h *ProductHandler) Create(c *gin.Context) {
    // 1. 解析 JSON 请求体并校验
    var request req.CreateReq
    if err := c.ShouldBindJSON(&request); err != nil {
        HandleValidationError(c, err)  // 校验失败,返回 400 + 字段级错误
        return
    }
    
    // 2. 将请求数据转为 Model 对象
    product := &model.Product{
        Name:   request.Name,
        Price:  request.Price,
        Stock:  request.Stock,
        Status: 1,  // 默认启用
    }
    
    // 3. 调用 Service 层
    if err := h.productSvc.Create(c.Request.Context(), product); err != nil {
        HandleServiceError(c, err)  // 统一错误处理
        return
    }
    
    // 4. 记录日志
    logger.WithCtx(c.Request.Context()).Infow("product created", 
        zap.Uint("product_id", product.ID))
    
    // 5. 返回成功响应
    response.Success(c, product)
}

第 4 步 --- Service 层internal/service/product_svc.go):

go 复制代码
func (s *productService) Create(ctx context.Context, product *model.Product) error {
    // 1. 写入数据库
    if err := s.repo.Create(ctx, product); err != nil {
        return errcode.ErrInternal().Wrap(err)  // 包装为统一错误码
    }
    // product.ID 此时已被 GORM 回填(数据库自增 ID)
    
    // 2. 删除缓存(Cache Aside 策略)
    s.delCache(ctx, productCacheKey(product.ID))
    return nil
}

第 5 步 --- Repository 层internal/repository/product_repo.go):

go 复制代码
func (r *productRepo) Create(ctx context.Context, product *model.Product) error {
    return r.db.WithContext(ctx).Create(product).Error
    // GORM 自动生成:INSERT INTO products (name, price, stock, ...) VALUES (?, ?, ?, ...)
    // 执行后将数据库自增 ID 回填到 product.ID
}

第 6 步 --- 响应返回

json 复制代码
{
    "code": 0,
    "message": "success",
    "data": {
        "id": 221,
        "name": "测试商品",
        "price": 99.99,
        "stock": 100,
        "status": 1,
        "created_at": "2026-07-01T08:46:23+08:00"
    }
}

6.2 分页查询商品(GET /admin/v1/products?page=1&size=10)

Repository 层的分页实现:

go 复制代码
func (r *productRepo) List(ctx context.Context, page, size int) ([]model.Product, int64, error) {
    // 1. 参数纠偏(防御性编程)
    if page <= 0 { page = 1 }
    if size <= 0 { size = 10 }
    
    // 2. 计算总记录数
    var total int64
    r.db.WithContext(ctx).Model(&model.Product{}).Count(&total)
    
    // 3. 分页查询(Offset/Limit)
    var products []model.Product
    offset := (page - 1) * size
    err := r.db.WithContext(ctx).
        Offset(offset).          // 偏移量,跳过前 N 条
        Limit(size).             // 限制返回条数
        Order("id DESC").        // 按 ID 降序排列
        Find(&products).Error    // 执行查询
    
    return products, total, err
}

6.3 部分更新商品(PUT /admin/v1/products/:id)

支持 PATCH 语义:只更新传入的字段,未传的字段保持不变。

go 复制代码
// Handler 层:用指针类型区分"不更新"和"更新为零值"
func (h *ProductHandler) Update(c *gin.Context) {
    product, _ := h.productSvc.GetByID(ctx, id)  // 先查已有记录
    
    var request req.UpdateReq
    c.ShouldBindJSON(&request)
    
    // 指针非 nil 才更新(nil 表示"不修改该字段")
    if request.Name != nil   { product.Name = *request.Name }
    if request.Price != nil  { product.Price = *request.Price }
    if request.Stock != nil  { product.Stock = *request.Stock }
    
    h.productSvc.Update(ctx, product)
}

// Repository 层:用 map 更新避免零值陷阱
func (r *productRepo) Update(ctx context.Context, product *model.Product) error {
    // 为什么用 map 而不是传 struct?
    // GORM 的 Updates(struct) 会忽略零值字段(如 Status=0, Stock=0)
    // 这意味着你不能把 Status 改为 0 或将 Stock 改为 0
    // 用 map[string]interface{} 就不会有这个问题
    updates := map[string]interface{}{
        "name":        product.Name,
        "price":       product.Price,
        "stock":       product.Stock,     // 即使是 0 也能正确更新
        "status":      product.Status,    // 即使是 0 也能正确更新
        "description": product.Description,
        "category_id": product.CategoryID,
    }
    result := r.db.WithContext(ctx).Model(product).Updates(updates)
    if result.RowsAffected == 0 {
        return gorm.ErrRecordNotFound  // 没有匹配的记录
    }
    return result.Error
}

6.4 删除商品(DELETE /admin/v1/products/:id)

go 复制代码
func (r *productRepo) Delete(ctx context.Context, id uint) error {
    result := r.db.WithContext(ctx).Delete(&model.Product{}, id)
    // GORM 软删除:UPDATE products SET deleted_at = NOW() WHERE id = ?
    // 如果模型没有 DeletedAt 字段:DELETE FROM products WHERE id = ?
    if result.RowsAffected == 0 {
        return gorm.ErrRecordNotFound
    }
    return result.Error
}

7. 三层架构:Handler → Service → Repository

这是 Gonio 最核心的架构设计,也是企业级 Go 项目的标准分层方式。

7.1 架构图

复制代码
┌─────────────────────────────────────────────────────┐
│                    HTTP 请求                          │
└─────────────────────────┬───────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────┐
│  Handler 层 (internal/handler/)                     │
│  职责: 解析请求 → 调用 Service → 返回 HTTP 响应      │
│  不做: 数据库操作、业务逻辑、缓存操作                  │
│  依赖: Service 接口(自己定义的)                      │
└─────────────────────────┬───────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────┐
│  Service 层 (internal/service/)                     │
│  职责: 编排业务逻辑 → 调用 Repository → 处理缓存     │
│  不做: HTTP 相关操作(不接触 *gin.Context)           │
│  依赖: Repository 接口 + Cache 接口                  │
└─────────────────────────┬───────────────────────────┘
                          ▼
┌─────────────────────────────────────────────────────┐
│  Repository 层 (internal/repository/)               │
│  职责: 数据库 CRUD → 返回数据模型                    │
│  不做: 业务逻辑判断、HTTP 相关操作                    │
│  依赖: *gorm.DB(具体依赖,接口在包内定义)            │
└─────────────────────────┬───────────────────────────┘
                          ▼
                    ┌──────────┐
                    │  MySQL   │
                    │  Redis   │
                    └──────────┘

7.2 每层依赖的是接口,不是实现

关键设计:接口定义在使用方

Handler 层自己定义需要的 Service 接口:

go 复制代码
// internal/handler/product_handler.go
type productService interface {
    List(ctx context.Context, page, size int) ([]model.Product, int64, error)
    GetByID(ctx context.Context, id uint) (*model.Product, error)
    Create(ctx context.Context, product *model.Product) error
    // ... 只定义 Handler 需要的方法
}

Service 层依赖 Repository 接口(定义在 Repository 包中):

go 复制代码
// internal/service/product_svc.go 依赖 internal/repository 中定义的接口
type productService struct {
    repo  repository.ProductRepository  // 接口类型,不是具体实现
    cache cache.Cache                   // 接口类型
}

7.3 依赖注入容器

所有依赖的"接线"集中在 ServiceContext 中:

go 复制代码
// 来自 internal/svc/service_context.go

type ServiceContext struct {
    Config          *config.Config       // 全局配置
    ProductHandler  *handler.ProductHandler  // 商品 Handler
    UserHandler     *handler.UserHandler     // 用户 Handler
    AdminHandler    *handler.AdminHandler    // 管理员 Handler
    HealthHandler   *handler.HealthHandler   // 健康检查
    // ... 更多
}

func NewServiceContext(cfg *config.Config, db *gorm.DB, rdb database.RedisClient, mqPublisher *mq.Publisher) *ServiceContext {
    // 1. 创建 Repository(数据访问层)
    userRepo := repository.NewUserRepo(db)
    adminRepo := repository.NewAdminRepo(db)
    productRepo := repository.NewProductRepo(db)
    
    // 2. 创建 Cache(缓存层)
    cacheImpl := cache.NewRedisCache(rdb)
    
    // 3. 创建 Service(业务逻辑层,注入 Repository + Cache)
    userSvc := service.NewUserService(userRepo, rdb, cfg.JWT.Secret, cfg.JWT.Expire)
    adminSvc := service.NewAdminService(adminRepo, rdb, cfg.JWT.Secret, cfg.JWT.Expire)
    productSvc := service.NewProductService(productRepo, cacheImpl, cfg.Server.CacheExpire)
    
    // 4. 创建 Handler(HTTP 层,注入 Service)
    productHandler := handler.NewProductHandler(productSvc)
    userHandler := handler.NewUserHandler(userSvc)
    adminHandler := handler.NewAdminHandler(adminSvc)
    
    return &ServiceContext{
        ProductHandler: productHandler,
        UserHandler:    userHandler,
        AdminHandler:   adminHandler,
        // ...
    }
}

7.4 分层的核心价值

好处 说明
可测试性 每一层都可以独立测试,通过 Mock 接口模拟下层
可替换性 换数据库只需改 Repository,Service 和 Handler 不动
关注点分离 每层只关心自己的职责,代码清晰可维护
团队协作 不同开发者可以并行开发不同层

8. JWT 用户认证与授权

8.1 JWT 是什么?

JWT(JSON Web Token)是一种在客户端和服务端之间安全传递信息的紧凑格式。

JWT 结构(三段式):

复制代码
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJyb2xlIjoidXNlciJ9.qVXj4snT_Dk...
├── Header ──────┤├────── Payload ─────────┤├── Signature ───┤
  • Header:记录签名算法(Base64 编码的 JSON)

    json 复制代码
    {"alg": "HS256", "typ": "JWT"}
  • Payload:存放用户信息(叫 Claims,声明)

    json 复制代码
    {
      "user_id": 1,
      "username": "zhangsan",
      "role": "user",
      "exp": 1782873975,
      "iat": 1782866775
    }
  • Signature:用密钥对前两部分签名,防止被篡改

    复制代码
    HMAC-SHA256(base64(header) + "." + base64(payload), secret)

8.2 登录流程

复制代码
客户端                        服务端
  │                             │
  │  POST /login                │
  │  {username, password}       │
  │ ─────────────────────────►  │
  │                             ├─ 1. 查数据库获取用户
  │                             ├─ 2. bcrypt 验证密码
  │                             ├─ 3. 生成 JWT Token
  │                             ├─ 4. 返回 Token
  │  {token, expire_at}         │
  │ ◄─────────────────────────  │
  │                             │
  │  GET /products              │
  │  Authorization: Bearer xxx  │
  │ ─────────────────────────►  │
  │                             ├─ 1. 解析 Token
  │                             ├─ 2. 验证签名
  │                             ├─ 3. 检查角色权限
  │                             ├─ 4. 执行业务逻辑
  │  {data: [...]}              │
  │ ◄─────────────────────────  │

密码安全:使用 bcrypt 加密,数据库中只存哈希值:

go 复制代码
import "golang.org/x/crypto/bcrypt"

// 加密(Cost=10,约 2^10=1024 轮迭代)
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(plainPassword), 10)

// 验证
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(inputPassword))

8.3 Token 生成

go 复制代码
// 来自 internal/pkg/auth/jwt.go

func GenerateToken(userID uint, username, role, secret string, expire int) (string, int64, error) {
    expireAt := time.Now().Add(time.Duration(expire) * time.Second)
    
    claims := &Claims{
        UserID:   userID,          // 用户 ID
        Username: username,        // 用户名
        Role:     role,            // 角色:user 或 admin
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expireAt),  // 过期时间
            IssuedAt:  jwt.NewNumericDate(time.Now()), // 签发时间
        },
    }
    
    // 使用 HS256 算法签名
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(secret))
    
    return tokenString, expireAt.Unix(), err
}

8.4 JWT 认证中间件

go 复制代码
// 来自 internal/middleware/auth.go

// AppAuth 用户认证中间件
func (m *AuthMiddleware) AppAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Authorization 头提取 Token
        token := extractToken(c)
        
        // 2. 解析并验证 Token
        claims, err := parseToken(token, m.jwtSecret)
        if err != nil {
            response.Error(c, errcode.ErrUnauthorized())
            c.Abort()  // ⚠️ 停止后续处理
            return
        }
        
        // 3. 检查角色权限
        if claims.Role != "user" {
            response.Error(c, errcode.ErrForbidden())
            c.Abort()
            return
        }
        
        // 4. 注入 Claims 到 Context(后续 Handler 可以获取用户信息)
        c.Set(auth.ClaimsKey, claims)
        c.Next()  // ✅ 继续执行后续 Handler
    }
}

c.Abort() vs c.Next():

  • c.Abort() --- 停止执行后续中间件和 Handler,直接返回
  • c.Next() --- 继续执行下一个中间件/Handler

9. 中间件系统

9.1 什么是中间件?

中间件是 Gin 的请求拦截器,在请求到达 Handler 之前/之后执行逻辑。中间件像洋葱一样层层包裹 Handler:

复制代码
请求 → Middleware1 → Middleware2 → Middleware3 → Handler → Middleware3 → Middleware2 → Middleware1 → 响应

9.2 中间件注册顺序

Gonio 的全局中间件有严格的顺序依赖:

go 复制代码
// 来自 internal/router/router.go
r.Use(
    middleware.Recovery(),          // 1️⃣ 必须第一!捕获所有 panic
    middleware.RequestID(),         // 2️⃣ 生成 request_id,后续所有日志都用它
    middleware.I18n(),              // 3️⃣ 多语言
    middleware.CORS(corsOrigins),   // 4️⃣ 跨域处理
    middleware.SecurityHeaders(),   // 5️⃣ 安全 HTTP 头
    middleware.MaxBodySize(2<<20), // 6️⃣ 限制请求体大小(2MB),防 OOM
    middleware.ReqRespLogger(),     // 7️⃣ 请求/响应日志
    middleware.Metrics(),           // 8️⃣ Prometheus 指标采集
)

9.3 Recovery --- Panic 恢复

Go 的 panic 类似其他语言的异常,但不被捕获会导致进程崩溃。Recovery 中间件必须放在第一位:

go 复制代码
func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                // 从 panic 中恢复,记录日志,返回 500 错误
                // 进程不会崩溃,继续处理后续请求
                logger.Log.Errorw("panic recovered", "error", r)
                response.Error(c, errcode.ErrInternal())
                c.Abort()
            }
        }()
        c.Next()
    }
}

9.4 CORS --- 跨域资源共享

浏览器有同源策略,默认禁止跨域请求。CORS 中间件通过添加响应头来允许跨域:

go 复制代码
func CORS(origins []string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", origin)          // 允许的源
        c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Authorization,Content-Type")
        c.Header("Access-Control-Max-Age", "86400")              // 预检缓存 24h
        
        // OPTIONS 预检请求直接返回 204
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    }
}

9.5 安全 HTTP 响应头

防御常见的 Web 攻击:

go 复制代码
func SecurityHeaders() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("X-Content-Type-Options", "nosniff")         // 禁止 MIME 嗅探
        c.Header("X-Frame-Options", "DENY")                   // 禁止被嵌入 iframe
        c.Header("X-XSS-Protection", "1; mode=block")         // XSS 过滤器
        c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
        c.Header("Content-Security-Policy", "default-src 'self'") // CSP
        c.Next()
    }
}

9.6 RequestID --- 请求追踪

为每个请求生成唯一的 trace ID,贯穿整个请求生命周期:

go 复制代码
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 优先使用客户端传来的 X-Request-ID
        requestID := c.GetHeader("X-Request-ID")
        if requestID == "" {
            // 自动生成一个随机 ID
            requestID = generateRequestID()
        }
        c.Set("request_id", requestID)  // 注入 Context
        c.Header("X-Request-ID", requestID)  // 返回给客户端
        c.Next()
    }
}

10. 错误处理与错误码设计

Gonio 实现了企业级的错误码系统,是项目的一大亮点。

10.1 错误分类

分类 编码前缀 严重程度 日志级别 示例
系统错误 1xxxx 告警 Error 数据库连接失败 (10001)
认证错误 2xxxx 警告 Warn 密码错误 (20001)
业务错误 3xxxx 信息 Info 商品不存在 (30001)
验证错误 4xxxx 调试 Debug 参数校验失败

10.2 错误码表

go 复制代码
// 来自 internal/pkg/errcode/errcode.go

// ── 系统错误(1xxxx) ──
ErrInternal()           // 10001 --- 内部错误(数据库/MQ/Redis 异常)
ErrBadRequest()         // 10002 --- 请求格式错误
ErrUnauthorized()       // 10003 --- 未认证/未登录
ErrForbidden()          // 10004 --- 已认证但无权限
ErrNotFound()           // 10005 --- 资源不存在
ErrTooManyRequests()    // 10006 --- 请求过于频繁(被限流)
ErrRequestBodyTooLarge()// 10010 --- 请求体过大
ErrInvalidParam()       // 10014 --- 参数不合法

// ── 认证错误(2xxxx) ──
ErrUserOrPassword()     // 20001 --- 用户名或密码错误(用户端)
ErrUserDisabled()       // 20002 --- 用户已被禁用
ErrAdminOrPassword()    // 20003 --- 管理员账号或密码错误
ErrAdminDisabled()      // 20004 --- 管理员已被禁用
ErrTokenExpired()       // 20005 --- Token 已过期
ErrTokenInvalid()       // 20006 --- Token 无效

// ── 业务错误(3xxxx) ──
ErrProductNotFound()    // 30001 --- 商品不存在
ErrProductOffShelf()    // 30002 --- 商品已下架
ErrStockNotEnough()     // 30003 --- 库存不足

10.3 AppError 不可变模式

错误对象创建后不可修改,所有方法返回新实例:

go 复制代码
// 创建错误
err := errcode.ErrProductNotFound()

// 包装原始错误(用于日志追踪)
err = errcode.ErrInternal().Wrap(originalDBError)

// 附加元数据
err = errcode.ErrInvalidParam().
    WithMeta("field", "id").
    WithMeta("raw_value", "abc")

// 最终返回给 Handler 的统一响应:
// {
//   "code": 10014,
//   "message": "参数不合法",
//   "trace_id": "a1b2c3d4",
//   "details": {"field": "id", "raw_value": "abc"}
// }

10.4 统一错误响应

所有错误响应都包含 trace_id,方便排查问题:

go 复制代码
// 成功响应
{"code": 0, "message": "success", "data": {...}}

// 错误响应
{"code": 20001, "message": "用户名或密码错误", "trace_id": "a1b2c3d4"}

非生产环境额外返回 details(元数据),帮助调试:

json 复制代码
{
  "code": 10012,
  "message": "用户名为必填字段",
  "trace_id": "xxx",
  "details": [{"field": "Key", "message": "'LoginReq.用户名' Error"}]
}

11. 配置管理与多环境

11.1 两层配置覆盖

复制代码
config/config.yaml          ← 基础配置(所有环境共用、默认值)
      ↓ 被覆盖
config/config_{env}.yaml    ← 环境覆盖配置(由 APP_ENV 控制,dev/integration/prod)

加载逻辑:

go 复制代码
func Load() (*Config, error) {
    // 1. 先加载基础配置
    yaml.Unmarshal(baseBytes, &cfg)
    
    // 2. 再加载环境配置覆盖
    env := os.Getenv("APP_ENV")  // 默认 "dev"
    yaml.Unmarshal(envBytes, &cfg)
    
    // 3. 校验必填项
    cfg.Validate()
    return &cfg, nil
}

11.2 支持的配置项

yaml 复制代码
server:
  port: 8080                # HTTP 端口
  mode: debug               # Gin 模式:debug/release/test
  read_timeout: 30          # 读取超时(秒)
  write_timeout: 30         # 写入超时(秒)
  auto_migrate: true        # 是否自动建表
  cache_expire: 600         # 缓存过期(秒)
  cors_origins: ["*"]       # 允许跨域的源

mysql:
  mode: single              # single(单机)或 multi(多实例)
  host: 127.0.0.1
  port: 3306
  database: YZY_DB
  max_idle_conns: 10        # 连接池:最大空闲连接
  max_open_conns: 100       # 连接池:最大打开连接
  prepare_stmt: true        # 预编译语句缓存
  skip_default_transaction: true  # 跳过默认事务

redis:
  mode: standalone          # standalone(单机)或 cluster(集群)
  addr: 127.0.0.1:6379
  db: 0
  pool_size: 100            # 连接池大小
  max_retries: 3            # 重试次数

jwt:
  secret: "your-secret-key" # 签名密钥(生产环境必须换!)
  expire: 7200              # Token 过期时间(秒)= 2 小时

log:
  mode: dev                 # dev(控制台)或 prod(JSON 文件)
  level: info               # 日志级别
  log_dir: ./logs           # 日志文件目录

security:
  enable_hsts: false        # 是否启用 HSTS
  hsts_max_age: 31536000    # HSTS 有效期(秒)

12. 结构化日志与请求追踪

12.1 Zap 日志库

Gonio 使用 Uber 开源的高性能结构化日志库 zap

go 复制代码
import "go.uber.org/zap"

// 结构化日志(非字符串拼接)
logger.Log.Infow("product created",
    zap.Uint("product_id", 221),
    zap.String("product_name", "测试商品"),
    zap.Float64("price", 99.99),
)

// 输出(JSON 格式):
// {"level":"info","msg":"product created","product_id":221,"product_name":"测试商品","price":99.99}

12.2 全链路追踪

request_id 通过 Context 贯穿整个请求生命周期:

go 复制代码
// 每个 Handler 都应该用 WithCtx 获取带 request_id 的 logger
logger.WithCtx(c.Request.Context()).Infow("processing request")
// 输出中自动包含 "request_id": "a1b2c3d4"

12.3 日志滚动

生产环境的日志文件按大小和时间自动切割(使用 lumberjack):

go 复制代码
// 单文件最大 100MB,保留 7 个备份,保留 30 天,自动压缩
lumberjack.Logger{
    Filename:   "./logs/app.log",
    MaxSize:    100,   // MB
    MaxBackups: 7,
    MaxAge:     30,    // 天
    Compress:   true,
}

13. 缓存策略与缓存穿透防护

13.1 Cache Aside(旁路缓存)模式

Gonio 使用最常见的缓存策略 --- Cache Aside:

复制代码
读取流程:
  1. 查缓存(GET key)
  2. 命中 → 直接返回
  3. 未命中 → 查数据库
  4. 将结果写入缓存 → 返回

写入流程:
  1. 更新数据库
  2. 删除缓存(不是更新缓存!)

为什么是删除缓存而不是更新?

  • 更新缓存可能失败 → 缓存中是旧数据
  • 删除缓存简单可靠 → 下次 GET 自动从 DB 加载最新数据

13.2 缓存穿透防护

问题:大量请求查询数据库中不存在的 ID,每次请求都"穿透"缓存打到数据库。

解决方案 --- 空值缓存

go 复制代码
func (s *productService) GetByID(ctx context.Context, id uint) (*model.Product, error) {
    // 1. 从缓存读取
    cached, err := s.cache.Get(ctx, "product:99999")
    if err == nil {
        if cached == "null" {
            // 空缓存命中 → 之前查过,不存在 → 不穿透 DB
            return nil, errcode.ErrProductNotFound()
        }
        // 正常缓存命中
        json.Unmarshal([]byte(cached), &product)
        return &product, nil
    }
    
    // 2. 缓存未命中 → 查数据库
    product, err := s.repo.GetByID(ctx, id)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            // 不存在 → 写入空缓存(60 秒过期)
            s.setCache(ctx, "product:99999", "null", 60*time.Second)
            return nil, errcode.ErrProductNotFound()
        }
        return nil, errcode.ErrInternal().Wrap(err)
    }
    
    // 3. 回写缓存
    data, _ := json.Marshal(product)
    s.setCache(ctx, "product:99999", string(data), s.cacheExpire)
    return product, nil
}

13.3 缓存 Key 命名规范

复制代码
product:123        --- 商品详情(业务前缀:ID)
user:456          --- 用户信息
rate_limit:ip:xxx --- 限流计数

使用冒号分隔的层级结构,方便通过 product:* 批量管理。


14. 限流器 Rate Limiter

14.1 令牌桶算法

Gonio 使用令牌桶算法限制请求频率,防止恶意攻击:

复制代码
令牌桶工作原理:
  ┌─────────────┐
  │  每 1/N 秒  │ → 放入一个令牌(速率 rate)
  │  补充令牌    │
  └─────────────┘
        ↓
  ┌─────────────┐
  │  桶容量: N   │ → 最多存 N 个令牌(burst 容量)
  │  tokens      │
  └──────┬──────┘
         ↓
  请求到达 → 取令牌
    ├─ 有令牌 → 放行
    └─ 无令牌 → 拒绝(429 Too Many Requests)

14.2 使用方式

go 复制代码
// 在路由组上应用限流中间件
admin := r.Group("/admin/v1")
admin.Use(middleware.RateLimit(svcCtx.RateLimiter))
{
    admin.POST("/products", productH.Create)
    admin.PUT("/products/:id", productH.Update)
    // ...
}

14.3 限流响应

被限流的请求返回:

json 复制代码
{
    "code": 10006,
    "message": "请求过于频繁",
    "trace_id": "xxx"
}

同时设置 Retry-After 响应头,告诉客户端多久后可以重试。


15. Prometheus 监控指标

Gonio 内置 Prometheus 指标采集,通过 /metrics 端点暴露数据。

15.1 指标类型

go 复制代码
// Counter(计数器,只增不减)
http_requests_total{method="GET", path="/products", status="200"} 12345

// Gauge(仪表盘,可增可减)
db_connections_open 42
db_connections_idle 8

// Histogram(直方图,分布统计)
http_request_duration_seconds_bucket{le="0.1"} 1000
http_request_duration_seconds_bucket{le="0.5"} 5000

15.2 监控的维度

指标 类型 含义
HTTP 请求总数 Counter 按 method + path + status 分组
HTTP 请求耗时 Histogram P50/P95/P99 分布
数据库连接数 Gauge 空闲/使用中/总数
Redis 连接数 Gauge 连接池状态

15.3 连接池监控

每 15 秒自动采集 MySQL 和 Redis 连接池指标:

go 复制代码
// 启动后台 goroutine 定时采集
go func() {
    ticker := time.NewTicker(15 * time.Second)
    for range ticker.C {
        stats := db.DB().Stats()
        dbOpenConnections.Set(float64(stats.OpenConnections))
        dbIdleConnections.Set(float64(stats.Idle))
        dbInUseConnections.Set(float64(stats.InUse))
    }
}()

16. 消息队列 MQ

16.1 概念

消息队列(Message Queue)用于服务间异步通信,实现解耦和削峰。

复制代码
生产者(Producer)→ MQ Broker → 消费者(Consumer)
   发消息                        收消息、处理

16.2 双驱动支持

Gonio 使用 Watermill 库,同时支持两种 MQ 后端:

  • Redis Streams:高性能,适合高吞吐场景
  • MySQL:无需额外部署(利用现有数据库),适合小规模场景

16.3 配置

yaml 复制代码
mq:
  driver: redis              # redis 或 mysql
  consumer_group: "gonio"   # Redis Streams 消费者组名
  topic_concurrency:         # 按 topic 配置并发消费者数
    product_created: 3
    order_created: 5
  default_max_len: 10000    # Stream 默认最大长度

17. 定时任务 Scheduler

17.1 Cron 表达式

Gonio 使用 robfig/cron 库,支持 6 字段秒级精度:

复制代码
秒 分 时 日 月 周
*  *  *  *  *  *

示例:
*/30 * * * * *    每 30 秒执行
0 */15 * * * *    每 15 分钟执行
0 0 3 * * *       每天凌晨 3 点

17.2 默认任务

go 复制代码
func RegisterDefaultTasks(sched *Scheduler) {
    sched.Register(TaskDef{
        Name:     "cache_cleanup",        // 缓存清理
        CronExpr: "0 */30 * * * *",       // 每 30 分钟
        Handler:  cleanExpiredCache,
    })
    sched.Register(TaskDef{
        Name:     "db_stats",             // 数据库统计
        CronExpr: "0 */15 * * * *",       // 每 15 分钟
        Handler:  collectDBStats,
    })
    sched.Register(TaskDef{
        Name:     "session_cleanup",      // 过期会话清理
        CronExpr: "0 0 * * * *",          // 每小时
        Handler:  cleanExpiredSessions,
    })
}

18. 安全最佳实践

18.1 密码安全

go 复制代码
// ✅ 正确:用 bcrypt 哈希存储密码
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

// ❌ 错误:绝不存储明文密码!
// password := "123456"

// ❌ 错误:不用 MD5/SHA1(太容易被暴力破解)
// hashed := md5.Sum([]byte(password))

18.2 SQL 注入防护

go 复制代码
// ✅ 正确:用占位符(参数化查询)
db.Where("username = ?", username).First(&user)

// ❌ 错误:字符串拼接
// db.Where("username = '" + username + "'").First(&user)
// 如果 username = "admin' OR '1'='1"
// 结果:WHERE username = 'admin' OR '1'='1' → 返回所有用户!

18.3 反时序攻击(防用户枚举)

登录时对"用户不存在"和"密码错误"返回相同的错误信息:

go 复制代码
// ✅ 正确:统一返回模糊错误
func (s *userService) Login(ctx context.Context, username, password string) (*LoginResult, error) {
    // 步骤 1:先查用户
    user, err := s.repo.GetByUsername(ctx, username)
    if err != nil {
        return nil, errcode.ErrUserOrPassword()  // 不区分原因
    }
    
    // 步骤 2:再验证密码
    if err := bcrypt.CompareHashAndPassword(...); err != nil {
        return nil, errcode.ErrUserOrPassword()  // 同样错误码
    }
    
    return &LoginResult{...}, nil
}

// ❌ 错误:暴露用户是否存在
// if userNotFound { return "用户不存在" }
// if passwordWrong { return "密码错误" }
// 攻击者可以通过错误信息枚举有效的用户名!

18.4 密码不出现在 JSON 响应中

go 复制代码
type User struct {
    Password string `json:"-"`  // json:"-" → JSON 序列化时忽略
}

19. 测试体系

19.1 测试类型

Gonio 有三种测试类型:

类型 位置 依赖 用途
单元测试(Mock) internal/*/ 无外部依赖 验证代码逻辑
HTTP 测试(Mock) test/ Mock Service 验证 Handler 层
集成测试 test/integration/ 真实 MySQL + Redis 验证全链路

19.2 Repository 层测试(sqlmock)

go 复制代码
func TestGetByUsername_Success(t *testing.T) {
    // 1. 创建 Mock 数据库
    db, mock, _ := sqlmock.New()
    gormDB, _ := gorm.Open(mysql.New(mysql.Config{Conn: db}))
    
    // 2. 设置预期 SQL 和返回数据
    rows := sqlmock.NewRows([]string{"id", "username", "password"}).
        AddRow(1, "testuser", "hashed_password")
    mock.ExpectQuery("SELECT .* FROM `users`").
        WillReturnRows(rows)
    
    // 3. 执行测试
    repo := NewUserRepo(gormDB)
    user, err := repo.GetByUsername(context.Background(), "testuser")
    
    // 4. 断言
    assert.NoError(t, err)
    assert.Equal(t, "testuser", user.Username)
}

19.3 Handler 层测试(httptest)

go 复制代码
func TestHealthCheck(t *testing.T) {
    router := gin.New()
    router.GET("/health", handler.Health)
    
    // 创建 HTTP 测试请求(不启动真实服务器)
    req := httptest.NewRequest("GET", "/health", nil)
    w := httptest.NewRecorder()
    
    // 执行请求
    router.ServeHTTP(w, req)
    
    // 断言响应
    assert.Equal(t, 200, w.Code)
}

19.4 运行测试命令

bash 复制代码
# 运行所有单元测试
go test ./... -count=1

# 运行集成测试(需要 MySQL + Redis 运行中)
APP_ENV=integration go test ./test/integration/... -v -count=1

# 带覆盖率
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out