AI时代 GO通用设计规范

作者: 分类: php 时间: 2026-08-25 评论: 暂无评论

Go 功能通用设计规范(AI 编码助手版)

版本:v1.0 | 适用对象:AI 编码助手(Claude / Copilot / Codex 等) | 适用范围:本仓库所有 Go 功能开发
阅读时机:每次接到新功能 / 修改 / 修复任务时,先通读本规范,再动手。
配套文档:开发文档.md(项目架构与协议)、docs/gomoku.proto(协议源)、docs/schema.sql(建表脚本)。


0. 如何使用本规范

本规范是 AI 写 Go 功能时的底线条款,按以下优先级裁决:

  1. 用户当前要求 > 本规范 > 个人编码习惯:用户明确要求了某种做法时,按用户说的做。
  2. 本规范 > 通用"最佳实践":不要拿网上搜来的风格覆盖本仓库约定。
  3. 与现有代码冲突时:以现有代码的实际约定为准(先读代码确认),并把差异写进交付说明,不要静默替换。

一句话原则:先读后写、边界先行、小步验证、不留垃圾。


1. 功能开发标准流程

AI 完成任何一个功能,必须按以下顺序推进,不得跳步:

步骤动作产出
1. 理解现状读相关代码、协议、文档;找到最相似的既有实现明确"改哪里、不动哪里"
2. 拆解计划把功能拆成可独立验证的小步骤步骤清单(写进任务列表)
3. 边界清单按 §2 列出该功能的全部边界输入与状态边界用例清单
4. 实现写代码:入口校验 → 核心逻辑 → 错误处理 → 日志编译通过的代码
5. 验证go buildgo vetgo test → 冒烟(§3)全绿
6. 收尾审查 diff、清理调试代码、同步文档/协议干净的最终 diff

铁律

  • 未读明白现有代码前,禁止开始写实现(禁止臆造 API、函数签名、字段名)。
  • 每完成一步立即验证,禁止"写完一大堆再一起编译"。
  • 禁止在提交里夹带与本功能无关的改动(顺手重构、改名、格式化无关文件)。

2. 边界检查规范(重点)

2.1 总则

  • 每个对外入口(组件方法、HTTP handler、RPC、消息处理)第一件事必须是参数校验,非法输入直接返回错误码,不得进入核心逻辑
  • 边界值不是"不可能发生":客户端永远不可信,本仓库所有对局裁决以服务端为准(见《开发文档》§5)。
  • 每个边界都必须有对应测试用例(见 §6)。

2.2 输入类边界

边界类型必须检查的内容本仓库实例
坐标/索引是否在 [0, BoardSize) 内、是否整数落子坐标 x,y 越界(棋盘 15×15,common.BoardSize
数值范围负数、0、上限、溢出计时参数、积分变化、步数
切片/数组nil、空、长度、索引越界、访问前是否判 lenmoves 回放、匹配队列
字符串空串、超长、非法 UTF-8、首尾空白uid、房间号、token
枚举/身份未知值、越界值、非法组合seat(0/1)、color(1=黑 2=白,落子接口不接收、由座位推导)、房间状态(RoomPlaying/RoomOver
引用nil 指针、nil 接口、nil map/slice组件内 db/rdb 为 nil 时降级返回(见 main.go openMySQL/openRedis)
幂等键重复请求(重复落子、重复匹配、重复结算)同一坐标二次落子必须拒绝

2.3 状态与流程边界

  • 状态机:任何状态迁移必须检查当前状态合法(如房间状态为 common.RoomPlaying 才允许落子;结束状态 common.RoomOver 才允许结算)。禁止假设调用顺序正确。
  • 重复调用:功能必须可重复进入(重连、重复 push、重复通知),要么幂等,要么明确拒绝。
  • 空态:空棋盘、空房间、空队列、空排行榜都要有定义好的行为(不能 panic、不能死循环)。

2.4 并发与资源边界(Go 特有)

  • 共享可变状态必须加锁:本仓库对局内存态由 service.RoomManager每房间一把 sync.Mutex 保护,新功能若读写共享状态,必须沿用同一把锁,禁止另起全局锁或裸读写。
  • goroutine 必须可退出:time.AfterFunc / ticker 等必须在房间结束时 Stop,禁止泄漏计时器。
  • channel 禁止对已关闭 channel 发送;关闭 channel 的责任方必须唯一。
  • 禁止并发写同一个 map/slice;写 nil map 会 panic。

2.5 时间与超时边界

  • 所有对局计时以服务器时钟为准,禁止信任客户端上报时间。
  • 时间比较用 time.Now().Before/After,不要用 ==UnixNano() 差值猜精度。
  • 外部调用(MySQL/Redis)必须带 context 超时(见 main.gocontext.WithTimeout 写法)。
  • 定时器回调里必须复查条件(房间仍为对应状态)再动作,防止"已经结束/已经销毁"后仍触发。

2.6 边界检查写法模板

入口统一校验,错误码用仓库统一错误码(internal/common/errors.goCode* 常量),不要返回裸字符串。新功能请仿照本仓库真实实现 internal/app/room/room.goPlace(组件层校验)与 internal/service/room.goRoom.Place(房间层在锁内校验),风格如下:

// 组件层:入口校验,顺序为 会话 → 引用 → 前置状态
func (c *Component) Place(ctx context.Context, in *model.C2SPlacePiece) (*model.S2CPlacePiece, error) {
    s := c.app.GetSessionFromCtx(ctx)
    if s == nil || s.UID() == "" {                       // 1) 身份边界
        return &model.S2CPlacePiece{Code: common.CodeNotLoggedIn, Msg: "未登录"}, nil
    }
    if in == nil {                                       // 2) 引用边界
        return &model.S2CPlacePiece{Code: common.CodeBadParam, Msg: "参数错误"}, nil
    }
    // 3) 数值边界:坐标越界在房间层一并校验(见下),组件层只做必须的浅校验
    // 4) 核心逻辑(房间层在锁内完成状态/回合/坐标校验)
}

// 房间层:锁内完成全部状态与输入校验(参照 service.Room.Place)
func (r *Room) Place(uid string, x, y int32) *model.S2CPlacePiece {
    r.mu.Lock()
    defer r.mu.Unlock()
    resp := &model.S2CPlacePiece{}
    if r.State != common.RoomPlaying {                   // 状态边界
        resp.Code, resp.Msg = common.CodeRoomState, "房间状态不允许"
        return resp
    }
    if seat := r.seatOf(uid); seat < 0 {                 // 身份边界
        resp.Code, resp.Msg = common.CodeRoomNotFound, "不在本房间"
        return resp
    } else if seat != r.TurnSeat {                       // 回合边界
        resp.Code, resp.Msg = common.CodeNotYourTurn, "非己方回合"
        return resp
    }
    if x < 0 || x >= common.BoardSize ||                 // 数值边界(坐标)
       y < 0 || y >= common.BoardSize ||
       r.Board[x][y] != 0 {                              // 占位冲突
        resp.Code, resp.Msg = common.CodeIllegalMove, "坐标越界或已有棋子"
        return resp
    }
    // ... 核心逻辑;返回错误时附带上下文(见 §5)
}

2.7 边界用例必须进测试

每条边界(越界 ±1、空、nil、未知枚举、重复调用、并发触发)都要在 *_test.go 里出现,否则该功能不算完成(见 §6 与 §8 DoD)。


3. 冒烟测试规范(重点)

3.1 定义

冒烟测试 = 最小闭环验证:不追求全覆盖,只验证"代码能编译、服务能启动、主链路能走通"。每个功能完成后必须跑,功能有破坏性改动时重跑。

3.2 冒烟分级

级别内容命令 / 动作失败即
L0 构建冒烟编译、静态检查、单测go build ./...go vet ./...go test ./...立即修复,禁止继续
L1 启动冒烟服务能带配置启动go run . -c config/config.yaml,日志出现 starting gomoku server,无 panic修复启动问题
L2 链路冒烟主业务链路端到端见 §3.3 清单报告并修复

3.3 本仓库 L2 冒烟清单(对弈主链路)

启动服务后(依赖:本地 Redis;MySQL 缺失时登录会降级,冒烟不依赖它),按序验证:

  1. 连接:WebSocket 连上 ws://127.0.0.1:3250(端口以 config/config.yaml 为准)。
  2. 登录auth.login 成功返回 uid/session(可用 web/index.html 客户端)。
  3. 匹配:两个账号 match.join 进入同一房间,收到 room.onMatchSuccess
  4. 对弈:黑方 room.place 落子成功 → 广播 room.onPlaceroom.onTurn → 白方落子成功;非法落子(越界、重复占位、非己方回合、观战者落子)均被拒绝并返回对应错误码(CodeIllegalMove/CodeNotYourTurn 等)。
  5. 结算:一方五连后广播 room.onGameOver(含制胜连线),记录落库(MySQL 可用时)。
  6. 重连(改动涉及房间状态时):对局中掉线再 room.reconnect,能恢复棋盘与计时。

3.4 冒烟脚本要求

  • 冒烟步骤要能重复执行:写成脚本或固定操作序列,禁止"手点一次碰运气"。
  • 冒烟输出要留证据:日志片段、测试输出,写进交付说明。
  • 冒烟发现问题必须修复后重跑整条链路,禁止只修单点不回归。

3.5 什么时候必须跑

  • 任何功能完成后(L0 必跑,涉及业务逻辑加跑 L2)。
  • 改动协议、序列化、房间状态机、计时、重连相关代码后,至少重跑 L1 + 对应链路
  • 改动 go.mod / 依赖版本后:L0 全量 + 启动冒烟。

4. AI 常见错误与禁令(重点)

以下按类别列出 AI 编码时的高频错误,每条的禁令必须遵守。

4.1 理解与规划类

错误原因正确做法
不读代码就写,臆造 API/签名/字段名凭印象编码先 grep 现有调用点,确认函数真实签名
重复造轮子没发现已有实现本仓库已有 common.FindWinLine、房间管理器、统一错误码,优先复用
过度设计引入不必要的抽象/依赖/设计模式功能优先最小实现;新依赖必须说明理由并经用户确认
顺手改动无关代码没控制 diff 范围只改本功能相关文件,收尾审查 diff

4.2 Go 语言陷阱类

陷阱说明正确做法
忽略错误返回值(_ = err / 不判 err)静默失败,后续逻辑建立在错误结果上所有返回 error 的调用必须处理(§5)
循环变量捕获for i := range xsgo func(){...i...} 闭包捕获同一变量循环体内显式 i := i,或直接传参
slice 共享底层数组a := b[:2] 后 append 可能改写 b 的底层数据需要独立数据时用 copyappend([]T{}, ...)
并发写 map / 写 nil map运行期 panic(concurrent map writes加锁;初始化用 make
向已关闭 channel 发送panic明确关闭责任方;用 select/哨兵关闭信号
defer 写在循环里资源句柄累积到函数结束才释放循环体内用匿名函数包住 defer,或改为显式 close
整数溢出计时、积分、时间戳计算溢出大数用 int64;溢出点加边界判断
string/[]byte 互转频繁每次转换都有拷贝热路径避免反复转换
值拷贝大结构体Board [15][15]int 直接值传递拷贝传指针(见 FindWinLine(b *Board, ...)

4.3 并发类

  • 禁止在没有锁的情况下读写共享房间状态。
  • 禁止"先释放锁再继续读共享数据"造成竞态;锁内完成判断与写入。
  • 禁止启动无法停止的 goroutine / 定时器;房间销毁时必须清理。
  • 禁止在持有锁的情况下执行阻塞 IO(DB/Redis 调用),会导致全房间卡死。

4.4 数据与存储类

  • 禁止把敏感信息(密码、连接串)写进代码或提交;生产配置走环境变量/密钥管理(见《开发文档》§2 安全提醒)。
  • 禁止修改表结构/协议字段而不同步 docs/schema.sql / docs/gomoku.proto
  • 禁止假设 MySQL/Redis 永远可用:本仓库约定连接失败按 nil 降级返回错误码(见 main.go),新功能沿用。
  • 写 Redis/MySQL 的 key 命名、TTL 要符合仓库现有约定,禁止自造一套。

4.5 测试类

  • 禁止只测 happy path:必须包含 §2.7 的边界用例。
  • 禁止测试依赖真实外部服务(MySQL/Redis 不可用时测试也要能跑);外部依赖用接口/mock 隔离。
  • 禁止"测试断言过弱"(如只断言不 panic):要断言返回值、错误码、状态变化。
  • 禁止新增代码没有对应测试(至少覆盖核心分支与边界)。
  • 禁止为凑覆盖率写无意义断言。

4.6 提交与协作类

  • 禁止提交编译不过的代码。
  • 禁止提交调试输出(fmt.Printlnlog.Println 临时打印、注释掉的代码块)。
  • 禁止提交密钥、.env、生成文件、无关产物(err.log、日志、二进制)。
  • 禁止擅自升级/增删依赖(本仓库锁定 Pitaya v2.11.24 等版本,升级需用户确认)。
  • 禁止在注释/文档里说谎:注释必须与代码行为一致,文档更新必须与代码同步。

5. 错误处理与日志

5.1 错误必须被处理

  • 所有 error 返回值必须检查:要么处理、要么带上下文向上返回。
  • 禁止用 _ 丢弃错误后继续关键逻辑。
  • 降级路径(db/rdb 为 nil)必须在入口处显式返回错误码,禁止在深层 panic。

5.2 错误包装

  • 内部错误用 fmt.Errorf("...: %w", err) 保留根因;对用户只暴露统一错误码。
  • 错误码使用 internal/common/errors.go 中已有的 Code* 常量(如 CodeBadParamCodeIllegalMoveCodeRoomState);新增错误码要先加定义再使用,并保持命名/编号风格一致。

5.3 日志

  • 使用项目统一日志(logrus,见 main.go),禁止裸 fmt.Println 打日志。
  • 级别使用:Info(流程关键节点)、Warn(可降级/可恢复)、Error(功能失败)、Fatal(仅启动致命错误)。
  • 日志必须带上下文(uid、roomId、请求参数摘要),便于回放问题。

5.4 panic 政策

  • panic 只允许用于不可恢复的编程错误(如断言失败),禁止用 panic 做业务控制流。
  • 业务异常一律返回 error。
  • 不允许新增代码在正常输入下触发 panic;运行期 panic 属 bug,必须修复而不是兜底吞掉。

6. 测试规范

  1. 表驱动:多个用例用 t.Run(name, ...) 表驱动写法(参照 internal/common/board_test.go)。
  2. 命名Test<函数名>_<场景>,用例 name 用中文描述场景(仓库现有风格)。
  3. 必测清单(每项都要出现):

    • 正常主路径(成功);
    • 每个边界(§2 表格逐条);
    • 错误路径(非法输入 → 正确错误码);
    • 状态机非法迁移被拒绝;
    • 重复/并发调用(幂等或被拒绝)。
  4. 隔离:单元测试不连 MySQL/Redis;需要时用接口注入或本地假实现。
  5. 回归:改动既有函数,其全部既有测试必须保持通过;有行为变更必须同步改测试并说明。

7. 代码风格与提交

7.1 强制工具

  • 提交前必须:go build ./...go vet ./...gofmt(代码必须 gofmt 通过)。
  • 文件内 import 分组保持仓库现状(标准库 / 第三方 / 本仓库),不混排。

7.2 命名与注释

  • 标识符用英文;注释可用中文(仓库现状如此),但必须准确,禁止复制粘贴错误注释。
  • 魔法数必须定义成命名常量(参照 common.BoardSizecommon.WinCountcommon.SeatBlack 等),禁止散落裸数字。
  • 新文件头部注释说明职责;导出函数写清楚参数与返回值语义。

7.3 提交纪律

  • 一次提交只做一件事;commit message 说明"做了什么 + 为什么"。
  • 提交前自查 diff:无调试残留、无无关文件、无密钥。
  • 收尾必须更新受影响的文档(开发文档.mddocs/gomoku.protodocs/schema.sql)。

7.4 禁止乱动清单

以下内容改动前必须经用户确认:

  • go.mod / go.sum(依赖变更);
  • docs/gomoku.proto 中的字段编号与类型(破坏性协议变更);
  • docs/schema.sql 已有表结构;
  • 现有对外方法名与路由(如 auth.loginmatch.joinroom.placeroom.reconnectrank.list,见《开发文档》§7.3 路由表);
  • web/lib/ 下官方客户端文件(《开发文档》注明"原样引入,勿改")。

8. 完成定义(DoD)检查清单

功能交付前,逐项自检,全部通过才算完成:

  • [ ] 已读透相关现有代码与协议,未臆造 API;
  • [ ] go build ./...go vet ./... 通过;
  • [ ] go test ./... 全部通过,且新增/修改的测试覆盖 §2 全部边界用例;
  • [ ] 冒烟 L0 通过;涉及业务逻辑的功能 L2 链路冒烟通过(§3.3);
  • [ ] 对外入口全部做了参数校验(§2),错误走统一错误码;
  • [ ] 无并发竞态、无 goroutine/定时器泄漏、无阻塞锁内 IO;
  • [ ] 无调试输出、无密钥、无无关改动;
  • [ ] 涉及协议/表结构/文档时已同步更新(§7.3);
  • [ ] 最终 diff 已整体审查,只包含本功能所需改动。

本规范为通用底线,具体功能需求优先于本规范;与仓库现有代码冲突时以现有约定为准并明示差异。

AI时代的通用设计规范 适合PHP、MYSQL

作者: 分类: php 时间: 2026-08-17 评论: 暂无评论
# 设计规范(PHP + MySQL)

> 通用设计规范:适用于基于 PHP(现代框架,如 Hyperf / Laravel / Symfony)与 MySQL 的后端服务。
> 规范级别约定:**[必须]** 强制要求,代码评审不通过项;**[应该]** 推荐做法,默认遵循;**[建议]** 可选优化。
> 提交评审前,代码必须通过本规范全部 **[必须]** 项与已适用的 **[应该]** 项。

## 适用范围

- 本规范适用于以下场景,必须触发自检:
  - 新建表、修改表结构、设计库表时
  - 编写或优化 SQL(查询 / 插入 / 更新 / 分页)时
  - 生成 migration 脚本时
- 不适用于 PostgreSQL / MongoDB / Redis 等非 MySQL 场景

---

## 1. PHP 规范

### 1.1 语言与环境

- **[必须]** 使用受支持的 PHP 版本(≥ 8.1),每个文件首行声明 `declare(strict_types=1);`
- **[必须]** 使用 Composer 管理依赖并提交 `composer.lock`,环境构建基于锁文件,保证可复现
- **[必须]** 全项目统一时区(`date.timezone`),避免时区错位导致的时间类缺陷
- **[必须]** 使用框架能力(路由、ORM、容器、中间件、事件、队列)解决问题,不重复造轮子
- **[应该]** 业务代码中不使用全局变量、`$_GET/$_POST` 直接取值,统一经框架 Request 抽象
- **[应该]** 代码静态检查(phpstan / psalm 等)纳入 CI

### 1.2 编码风格与命名

- **[必须]** 遵循 PSR-12 编码风格
- **[必须]** 命名规则:
  - 类 / 接口 / Trait:`PascalCase`;接口后缀 `Interface`,抽象类前缀 `Abstract`
  - 方法 / 函数 / 变量 / 属性:`camelCase`
  - 常量(类常量 / 全局常量):`SCREAMING_SNAKE_CASE`
  - 布尔变量:`is` / `has` / `can` 语义前缀(如 `isLocked` / `hasToken` / `canDelete`)
  - 私有/受保护成员:无下划线前缀,用可见性关键字表达
- **[必须]** 类文件遵循 PSR-4 自动加载;一个文件只声明一个类
- **[必须]** 方法必须有参数类型与返回类型声明;可空类型用 `?type`;联合类型用 `type1|type2`
- **[必须]** 常量集中定义(业务阈值、TTL、状态值),禁止散落魔法数字
- **[应该]** 注释只解释「为什么」,不赘述「做了什么」;逻辑复杂处必须写注释说明意图
- **[必须]** 注释 / 文档描述必须与代码实际行为一致(如声称「加密存储」就必须真的加密)

### 1.3 分层架构

- **[必须]** 三层职责分离:`Controller → Service → Model`,依赖方向单向、不得跨层

  | 层 | 职责 | 禁止 |
  |---|---|---|
  | Controller | 参数接收、格式校验、调用 Service、统一响应 | 业务逻辑、SQL |
  | Service | 业务规则、流程编排、事务边界、审计 | SQL 细节、请求/响应对象污染 |
  | Model | 表映射、关系、查询作用域、类型转换 | 业务规则 |
- **[必须]** Controller 内禁止出现 SQL、循环、复杂条件分支;只做「取参 → 校验 → 调用 → 响应」
- **[必须]** 一个方法只做一件事,方法体超过约 60 行应拆分
- **[应该]** 相同实体聚合在同一 Service,禁止散落多处修改同一表

### 1.4 依赖注入

- **[必须]** 依赖通过构造函数注入,禁止在方法内部 `new` 核心服务、禁止静态方法承载业务逻辑
- **[必须]** 禁止在业务代码中直接使用服务容器全局访问(服务定位器反模式)
- **[应该]** 面向接口编程:核心依赖定义接口,实现可替换(便于 Mock 与测试)

### 1.5 输入校验

- **[必须]** 先校验、后处理:所有外部输入(请求体、查询参数、请求头、上传文件)先过校验再进入业务
- **[必须]** 使用白名单校验(格式、长度、枚举),校验失败返回语义化错误,不进入业务逻辑
- **[必须]** 对所有字符串输入做 `trim` 与类型强转,保证后续比较的类型一致
- **[必须]** 枚举类输入(如验证码用途、登录方式)必须限定合法取值集合
- **[应该]** 复杂校验抽为校验器/表单请求对象,避免散落在 Controller 各处

### 1.6 异常与错误处理

- **[必须]** 业务异常统一继承自定义异常基类,携带错误码与用户可读消息;禁止直接抛裸 `\Exception`
- **[必须]** HTTP 状态码语义化:

  | 状态码 | 语义 |
  |---|---|
  | 400 | 参数错误 / 校验失败 |
  | 401 | 未认证 / 凭证无效 |
  | 403 | 无权限(含演示功能开关未开启) |
  | 404 | 资源不存在 |
  | 409 | 冲突(唯一性、重复操作) |
  | 419 | CSRF 校验失败 |
  | 422 | 语义 / 业务规则不满足 |
  | 423 | 资源被锁定(账号锁定) |
  | 429 | 限流 / 频控 |

- **[必须]** 全局异常处理器兜底:5xx 响应禁止返回堆栈、SQL、框架内部信息;异常详情只进服务端日志
- **[必须]** 未匹配路由返回 JSON 404,不返回框架默认 HTML 页面
- **[必须]** 禁止 `die` / `exit` / 裸 `@` 抑制错误;禁止空 `catch` 吞异常(必须记录日志或重新抛出)
- **[必须]** 日志分级正确(debug/info/warn/error),禁止把密码、令牌、验证码等敏感信息写入日志

### 1.7 安全编码

- **[必须]** 数据库访问一律走 ORM / 参数绑定,禁止字符串拼接 SQL
- **[必须]** 密码使用 bcrypt(cost ≥ 12)或 argon2 哈希,禁止明文、MD5、SHA1
- **[必须]** 验证码、令牌等一次性凭证:服务端生成 `random_bytes`/`random_int`,只存哈希(加盐),比较用 `hash_equals`(防时序攻击),设有效期、次数上限、一次性使用
- **[必须]** 登录失败返回统一文案,不泄露账号是否存在(防用户枚举)
- **[必须]** 写操作必须防 CSRF(双提交令牌或同源校验),比较用常数时间函数
- **[必须]** 认证令牌:短时效 Access + 可吊销、轮换的 Refresh;令牌存 httpOnly Cookie(SameSite),响应与日志中不得出现明文令牌
- **[必须]** 设备/会话维度支持吊销,吊销后相关令牌立即失效
- **[必须]** 上传文件:扩展名 + MIME 白名单、大小上限、内容魔数校验、服务端生成随机文件名、存储目录不可执行脚本
- **[必须]** 密码、私钥、盐、第三方凭证必须来自环境变量/配置中心,禁止硬编码;禁止使用默认密钥上线
- **[必须]** 敏感个人信息(证件号、银行卡号,手机号等)明文落库,接口只返回脱敏值
- **[必须]** 手机号全库统一「区号 + 号码」两段式:`country_code VARCHAR(8)`(E.164 格式,如 `+86`)+ `phone VARCHAR(20)`(号码本体,不含 `+`),禁止把区号拼进号码列
- **[应该]** 登录等敏感路径全量写审计日志(成功/失败、原因、IP、UA、设备),审计记录只追加、不修改不删除

### 1.8 并发与事务

- **[必须]** 计数累加 / 状态流转等读改写操作必须原子化:优先 SQL 原子更新(`SET x = x + 1`)或带条件更新,禁止纯「读-改-写」三段式(并发丢失更新)
- **[必须]** 业务唯一性由数据库唯一约束兜底,代码只做友好提示,禁止仅靠「先查后插」保证唯一
- **[必须]** 一次性资源(验证码、二维码票据、兑换码)的「领取/作废」必须原子(条件更新或加锁),防止并发重复消费
- **[必须]** 事务保持短小:事务内禁止远程调用(HTTP、短信、邮件)与长查询;事务边界清晰,只包裹需要一致性的写操作
- **[应该]** 高频读写热点按场景选择乐观锁(版本号)或悲观锁(行锁 `FOR UPDATE`),避免表锁
- **[应该]** 对外写接口设计幂等键,重复请求不产生重复副作用

### 1.9 性能

- **[必须]** 查询避免 N+1:关联数据使用预加载
- **[必须]** 列表/分页接口必须有数量上限(`per_page` 上下限兜底),禁止无界查询
- **[必须]** 禁止循环内查询数据库 / 循环内发远程请求
- **[必须]** 大量数据写入使用批量插入,避免逐条 insert
- **[应该]** 高频只读数据使用缓存,并明确缓存失效与一致性策略

### 1.10 配置与环境

- **[必须]** 配置分层(dev / test / prod),敏感配置不入版本库(提供 `.env.example` 模板)
- **[必须]** 生产环境必须关闭调试开关(DEBUG、验证码明文回显、演示查询接口等),且此类开关要有运行时门禁(开启才可用,关闭必须 403/404)
- **[必须]** 配置值使用前做类型强转,避免字符串/布尔歧义

### 1.11 测试

- **[必须]** 认证、支付、权限等关键流程必须有自动化测试(单元 / 集成 / e2e)
- **[应该]** 测试覆盖:正常路径、边界条件(空值、超长、越界)、异常路径、并发竞态
- **[应该]** 修复 bug 时先补回归测试

---

## 2. MySQL 规范

### 2.1 存储引擎与字符集

- **[必须]** 统一 InnoDB(事务 + 行锁 + 崩溃恢复)
- **[必须]** 统一字符集 `utf8mb4`;MySQL 8 采用排序规则 `utf8mb4_0900_ai_ci`(MySQL 5.7 采用 `utf8mb4_unicode_520_ci`)
- **[必须]** 建表语句必须带 `ENGINE=InnoDB` 与 `COMMENT`,每个字段必须有 `COMMENT` 说明业务含义
- **[必须]** 禁止使用外键与级联:高并发下外键强阻塞、易引发更新风暴,约束由应用层保证
- **[必须]** 禁止用存储过程、触发器、视图承载核心业务逻辑(逻辑必须写在应用层)
- **[必须]** 大表 DDL 必须用在线变更工具(`gh-ost` / `pt-online-schema-change`),禁止直接 ALTER 大表;小表 DDL 用 `ALGORITHM=INPLACE`
- **[必须]** 时间字段一律用**正整数整数时间戳**存储:默认 `BIGINT UNSIGNED` 存 Unix 秒(UTC);`created_at` / `updated_at` 固定为秒级,其余时间字段在业务要求毫秒精度时,可用 `BIGINT UNSIGNED` 存 Unix 毫秒(列名不加后缀);每个字段的秒 / 毫秒语义确定后固定不变,跨字段比较时注意单位一致;展示时由应用层转换时区;禁止 `DATETIME` / `TIMESTAMP` / `DATE` / 字符串(杜绝时区语义分歧);纯日期场景(如生日)同样用时间戳表达

### 2.2 表设计

- **[必须]** 每表必须有主键:`id INT UNSIGNED AUTO_INCREMENT`(默认);预计行数接近 21 亿(signed INT 上限,保守阈值)的高增长表(如 message 消息表)使用 `id BIGINT UNSIGNED AUTO_INCREMENT`;主键仅 `id` 单字段;禁止 UUID / 字符串 / 业务唯一键替代主键(索引膨胀、耦合业务),业务唯一键一律用唯一索引表达;分布式强需求使用 UUID 主键须评审特批
- **[必须]** 每表必备三字段:
  - `id`(主键)
  - `created_at BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间(Unix 秒,0 表示未设置)'`
  - `updated_at BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间(Unix 秒,应用层维护,0 表示未设置)'`
  - 命名全库统一,禁止混用 `create_time` / `update_time` 等其他写法
  - 全库时间统一为**正整数 Unix 时间戳(`BIGINT UNSIGNED`)**:默认秒级(`0` 表示未设置);`created_at` / `updated_at` 固定秒级,其余时间字段业务要求毫秒精度时使用毫秒级(列名不加后缀);存储紧凑、无时区错位、比较与排序高效;展示格式化(`Y-m-d H:i:s`)在应用层完成;`updated_at` 由应用层(ORM 时间戳机制)维护,不使用 `ON UPDATE CURRENT_TIMESTAMP`
- **[必须]** 业务唯一性用唯一约束 / 唯一索引表达(`UNIQUE KEY`),并在应用层返回友好冲突提示
- **[必须]** 逻辑删除优先于物理删除:统一 `is_deleted TINYINT(1) NOT NULL DEFAULT 0`(全库唯一口径,禁止改用其他字段表达逻辑删除);须处理唯一键复用(如「手机号唯一」在记录已删除后的冲突策略)
- **[必须]** 表结构满足已声明的业务约束:如「密码绝不落明文」→ 表里只能有哈希列;「验证码哈希存储」→ 不能存在明文列
- **[必须]** 禁止将密码、令牌、验证码等凭证明文入库;敏感个人信息(证件号、银行卡号等)统一明文落库,接口只返回脱敏值
- **[必须]** 所有字段 `NOT NULL` + 合理默认值(仅业务必须可空时允许 NULL,如可选手机号)
- **[必须]** 状态字段用 `TINYINT` / `INT`(按取值范围选择)+ 应用层常量映射,取值不设上限,禁止无注释魔法值

### 2.3 命名规范

- **[必须]** 库名 / 表名 / 字段名:小写字母 + 数字 + 下划线,见名知意,**不超过 30 字符**,禁止拼音;表名用 `snake_case` 复数并带业务/模块前缀(如 `user_devices`、`login_logs`)
- **[必须]** 字段名:`snake_case`,语义完整清晰(如 `refresh_token_hash` 而非 `rtk`)
- **[必须]** 禁用 MySQL 保留字(`desc` / `range` / `match` / `key` / `order` 等),无法避免时用反引号
- **[必须]** 临时表 `tmp_xxx_YYYYMMDD`,备份表 `bak_xxx_YYYYMMDD`
- **[必须]** 索引命名:主键用 `PRIMARY KEY`;唯一索引 `uk_<字段>`、普通索引 `idx_<字段>`,跨表/多字段场景加表名缩写(如 `uk_user_phone`、`idx_user_created`)
- **[必须]** 同一字段在多表中的类型与命名必须完全一致(避免隐式转换致索引失效)
- **[应该]** 外键约束命名 `fk_<表>_<引用表>`(本规范禁用外键,此条仅约束遗留场景)

### 2.4 字段类型

- **[必须]** 按场景选择类型,禁止越权使用:

  | 场景 | 必须使用 | 禁止使用 |
  |---|---|---|
  | 布尔 | `TINYINT(1)`,取值只允许 0/1 | — |
  | 状态 / 枚举 | `TINYINT` 或 `INT`(按取值范围),取值不设上限,配应用层常量 | `ENUM` / 英文枚举值 |
  | 小整数 | `TINYINT` / `SMALLINT` | — |
  | 常规整数 | `INT` | — |
  | 主键 | `INT UNSIGNED AUTO_INCREMENT`(高增长表如 message 用 `BIGINT UNSIGNED`) | UUID / 字符串主键 |
  | 金额 / 汇率 | `DECIMAL(M,2)`,M 按业务定 | `FLOAT` / `DOUBLE`(精度丢失) |
  | 定长短字符串 | `CHAR(N)` | — |
  | 不定长字符串 | `VARCHAR(N)`,N ≤ 5000 | 一律 `VARCHAR(255)` |
  | 超长文本(商品详情等) | `TEXT` / `LONGTEXT`,拆到独立表用主键关联 | 塞进主表拖垮索引 |
  | 时间 | `BIGINT UNSIGNED`(默认 Unix 秒级;`created_at` / `updated_at` 固定秒级,其余字段业务要求毫秒精度时用 Unix 毫秒) | `DATETIME` / `TIMESTAMP` / `DATE`(本规范统一正整数 int 时间戳) |

- **[必须]** 证件号 / 银行卡号等定长数据不超必要长度,且落库不加密;**手机号两段式**:`country_code VARCHAR(8)`(E.164,如 `+86`)+ `phone VARCHAR(20)`(号码本体),唯一约束建在 `(country_code, phone)` 组合上
- **[必须]** 业务枚举值一律用 `TINYINT` / `INT`(按取值范围)+ 应用层常量映射;禁止 `ENUM` 与英文枚举值(如 `status = 'ACTIVE'`)

### 2.5 索引规范

- **[必须]** 单表索引 ≤ 5 个,单索引字段 ≤ 5 个
- **[必须]** 区分度低的列(`gender` / `status` / `is_deleted`)禁止单独建索引,可作组合索引后缀
- **[必须]** 组合索引遵循最左前缀:建立 `(a,b,c)` 即覆盖 `(a)` / `(a,b)` / `(a,b,c)`,避免重复建设
- **[必须]** WHERE / ORDER BY / GROUP BY 的字段应建索引,避免 `filesort`
- **[必须]** JOIN 列数据类型必须相同且都建索引,小表驱动大表
- **[必须]** 索引列上禁止函数运算与隐式类型转换(`WHERE DATE(created_at)=...`、字符串列查数字),会导致索引失效
- **[必须]** 唯一性约束用唯一索引,并作为并发写入的最终兜底
- **[必须]** 禁止冗余索引与重复索引(如已有 `idx(a,b)` 又建 `idx(a)`);新查询先 `EXPLAIN` 验证执行计划
- **[应该]** 大文本字段索引用前缀索引(`INDEX idx_title(title(100))`)
- **[应该]** 建索引前用 `SELECT COUNT(DISTINCT col) / COUNT(*)` 评估区分度
- **[应该]** 高频查询构造覆盖索引(`SELECT` 列包含在索引中)减少回表

### 2.6 SQL 编写规范

- **[必须]** 禁止 `SELECT *`,必须明确列出字段(减少 IO 与耦合):
-- 反例
SELECT * FROM orders WHERE user_id = 1000;
-- 正例
SELECT id, order_no, status, amount FROM orders WHERE user_id = 1000;

- **[必须]** INSERT 必须指定字段,禁止 `INSERT INTO t VALUES (...)` 无列名写法
- **[必须]** 深分页禁止 `LIMIT offset, size`,用游标分页:
-- 反例(深分页全扫)
SELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 20;
-- 正例(游标)
SELECT id, order_no FROM orders WHERE id < ? ORDER BY id DESC LIMIT 20;

- **[必须]** WHERE 索引列禁止函数 / 运算:
-- 反例(索引列套函数)
WHERE YEAR(FROM_UNIXTIME(created_at)) = 2026
-- 正例(半开区间,参数绑定)
WHERE created_at >= ? AND created_at < ?   -- ? 为应用层传入的 Unix 时间戳

- **[必须]** 禁止负向查询(`!=` / `NOT IN` / `NOT LIKE`)与 `%xxx` 前缀模糊查询(致全表扫描);优先等值 / 范围查询
- **[必须]** `IN` 列表 < 1000 项,超量改用 JOIN 或临时表
- **[必须]** 禁止 `ORDER BY RAND()`;禁止大表 JOIN 大表;避免子查询(用 JOIN 替代)
- **[必须]** 时间范围统一用半开区间 `>= AND <`,禁止 `BETWEEN` / `<=`(均含右边界,边界语义易歧义)
- **[必须]** 分页 / 批量查询必须有 `LIMIT` 上限
- **[必须]** 批量插入分批执行(单批数百~数千行),避免超大 SQL
- **[必须]** 锁范围最小化(`FOR UPDATE` 只锁必要行)
- **[必须]** 统计口径明确:`COUNT` 用明确的 `COUNT(*)` 或 `COUNT(列)`;大表计数禁止无过滤全表扫
- **[应该]** 大批量数据变更(更新/删除)分片执行,避免长时间持有锁与主从延迟
- **[应该]** 复杂聚合(趋势、分布统计)明确窗口与口径,必要时用汇总表 / 缓存结果

### 2.7 数据安全与合规

- **[必须]** 数据库账号最小权限:应用账号仅授本库 DML,管理账号与业务账号分离;禁止应用使用 root
- **[必须]** 敏感数据(个人信息)统一明文落库、接口脱敏返回、设置保留期与定期清理任务
- **[必须]** 存储的个人照片、证件扫描件等设置访问控制与生命周期(到期删除),并有清理任务实现(不允许只写注释不实现)
- **[应该]** 定期备份与恢复演练;审计表等只追加表按容量规划滚动清理/归档
- **[应该]** 慢查询日志开启,定期审查慢 SQL

### 2.8 性能与运维基线

- **[应该]** 连接使用连接池;应用与数据库间开启长连接复用
- **[应该]** 大表按时间/业务维度规划分区或归档策略
- **[应该]** 上线变更(加索引、改表)在低峰执行,DDL 使用在线算法 / 在线变更工具

### 2.9 建表 / 变更 SQL 产出流程

按以下步骤执行,每步自检通过再进入下一步:

1. 与业务确认实体、字段、关系、预期数据量与查询模式
2. 按「基础 → 命名 → 字段 → 表结构 → 索引」顺序逐条自检本规范
3. 输出完整 DDL(含 COMMENT、默认值、索引、引擎、字符集)
4. 附带 migration 脚本,必须幂等(`IF NOT EXISTS` / `INSERT IGNORE`),本地验证「全新安装 + 升级安装」两条路径
5. 用 `EXPLAIN` 验证关键查询命中索引:`type` 应为 `const` / `ref` / `range`,禁止 `ALL`

---

## 3. 评审入口与提交前检查清单

代码 / DDL 提交评审时逐条核对:

- [ ] 所有表、字段有 COMMENT,注释说明业务含义
- [ ] 主键存在且为 `INT UNSIGNED AUTO_INCREMENT`(高增长表为 `BIGINT UNSIGNED`),主键仅 `id` 单字段
- [ ] `id` / `created_at` / `updated_at` 三字段齐全(时间字段为 `BIGINT UNSIGNED` 时间戳),命名全库统一
- [ ] 无外键、无冗余索引、无重复索引、无保留字
- [ ] 金额字段为 `DECIMAL`,无 `FLOAT` / `DOUBLE` 存金额
- [ ] 所有字段 `NOT NULL` 有默认值(业务必须可空除外)
- [ ] 索引区分度已评估;单表索引 ≤ 5,区分度低列未单独建索引
- [ ] 无 `SELECT *`、无字符串拼接 SQL、无深分页 `OFFSET`、无负向查询
- [ ] migration 脚本幂等且本地通过(全新安装 + 升级安装)
- [ ] 关键 SQL 已 `EXPLAIN` 验证(type 非 ALL)
- [ ] 注释 / 文档声称与实现一致(声称加密、声称即删必须真实实现)
- [ ] 唯一性由数据库约束兜底,无「先查后插」;一次性资源消费原子化
- [ ] 无明文凭证数据(密码 / 令牌 / 验证码);敏感个人信息(证件号 / 银行卡号 / 手机号)接口已脱敏返回

### 禁止做的事(红线)

- 不要生成带外键的 DDL
- 不要用 `FLOAT` / `DOUBLE` 存金额
- 不要用 `SELECT *`
- 不要用字符串列做主键
- 不要在索引列上套函数
- 不要用存储过程 / 触发器 / 视图承载核心业务逻辑
- 不要一次性给出大批量未经自检的 DDL,必须逐表自检后再输出
- 不要硬编码密钥与默认密钥上线

go 的defer 执行顺序

作者: 分类: go 时间: 2026-08-11 评论: 暂无评论
package main

import "fmt"

// 核心规则4
// return 语句不是原子操作,它分为两步:

//第一步:将返回值(若有命名返回值则直接赋值,若无则创建临时变量)赋值为 return 后面的表达式。

//第二步:执行所有已注册的 defer 函数(后注册先执行)。

//第三步:函数真正返回。

//命名返回值(如 (r int))在函数开始时就已声明并初始化为零值,return 语句会先给这个变量赋值。

// defer 函数的参数在 defer 语句被执行到时就已经求值(值拷贝),而 defer 函数体内部对变量的修改是否影响外部,取决于它捕获的是变量本身(闭包)还是副本(参数)。

func f1() (r int) {
    defer func() {
        r++
    }()
    return 0 //r=0 再执行 defer  r=1
}

func f2() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t // 创建临时变量=5  t+5无效
}

func f3() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1 // r=1  defer 使用拷贝了一份,使用内部变量
}

func f4() (r int) {
    defer func() {
        r = r + 5
    }()
    return 1 // r=1  defer后=6
}

func main() {
    fmt.Println(f1()) //1
    fmt.Println(f2()) //5
    fmt.Println(f3()) //1
    fmt.Println(f4()) //6

}

GO多态

作者: 分类: go 时间: 2026-08-11 评论: 暂无评论

1.多态是什么
通俗讲就是 “一个接口,多种形态”。它指的是:同一个行为(方法名),作用于不同的对象时,会产生不同的执行结果。
2.举例子

// 定义一个函数,它接受 A 接口类型
func Process(a A) {
    // 这里不管传入的是谁,直接调用 ShowA
    fmt.Println(a.ShowA()) 
}

func main() {
    w1 := Work{3}   // ShowA 返回 i+10
    w2 := OtherWork{5} // 假设另一个结构体,ShowA 返回 i*2

    Process(w1) // 输出 13
    Process(w2) // 输出 10(假设 OtherWork 的 ShowA 是乘以2)
}

在这个例子里:

Process 函数只关心参数是 A 类型,它根本不知道传入的是 Work 还是 OtherWork。
但运行时,w1 和 w2 各自执行了自己独有的 ShowA 逻辑。

GO channel总结

作者: 分类: php 时间: 2026-05-29 评论: 暂无评论

1. Channel 四种读取方式

1.1 直接读取(无 ok 检测)

ch := make(chan int, 1)
v := <-ch // 不会 panic,但无法区分是真实零值还是关闭后零值

1.2 带 ok 检测的读取

v, ok := <-ch
// ok 为 true 时,表示成功从 channel 读到真实数据。
// ok 为 false 时,表示 channel 已关闭且无更多数据,此时 v 是类型零值。

1.3 range 循环读取

timer := time.NewTicker(5 * time.Second)
defer timer.Stop()
for v := range timer.C{
   fmt.Println(1)
}
  • 会持续从 channel 读取值,直到 channel 被关闭且数据读完,循环自动终止。
  • 等效于不断执行 v, ok := <-ch 并检查 ok,写法更简洁。
  • 注意:如果 channel 永远不关闭,会一直阻塞造成死锁(除非有其他退出机制)。

1.4 select 多路复用读取

select {
case v := <-ch:
    // 处理数据
default:
    // 非阻塞
}

timer := time.NewTicker(5 * time.Second)
defer timer.Stop()
for {
     select {
    case <-timer.C:
      fmt.Println(1)
     }
   }
  • 可以同时监听多个 channel,哪个有数据就执行对应的 case。
  • 配合 default 可实现非阻塞读取:若所有 channel 都无数据则立即执行 default。
  • 即使某个 channel 已关闭,读取也会立即得到零值(需结合 v, ok := <-ch 检测关闭)。

Channel 状态速记

操作nil channel正常 channelclosed channel
读取阻塞正常读取返回零值
写入阻塞正常写入panic
关闭panic正常关闭panic

读关零值,写关 panic;nil 两边都阻塞;无缓冲要配对,有缓冲看容量;关两次也要 panic。


2. sync.WaitGroup 等待多 goroutine

场景:汇总多个请求并发处理。如同时对多个用户发送通知、对多个文件压缩、大量图片缩略图生成、多 API 聚合。

2.1 基础用法

package main

import (
    "fmt"
    "sync"
    "time"
)

func work(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second * 2)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go work(i, &wg)
    }
    wg.Wait()
    fmt.Println("ALL DONE")
}

2.2 多服务聚合(带错误处理)

package main

func AggregateData(ctx context.Context) (Result, error) {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var userData *User
    var orderData *Order
    var errs []error

    wg.Add(3)

    // 调用用户服务
    go func() {
        defer wg.Done()
        data, err := callUserService(ctx)
        mu.Lock()
        userData = data
        if err != nil {
            errs = append(errs, err)
        }
        mu.Unlock()
    }()

    // 调用订单服务
    go func() {
        defer wg.Done()
        data, err := callOrderService(ctx)
        mu.Lock()
        orderData = data
        if err != nil {
            errs = append(errs, err)
        }
        mu.Unlock()
    }()

    // 调用库存服务(类似)

    wg.Wait()
    // 汇总结果或处理错误
    if len(errs) > 0 {
        return Result{}, combineErrors(errs)
    }
    return merge(userData, orderData), nil
}

3. 生产者-消费者模式(Channel 实现)

package main

import (
    "fmt"
    "math/rand"
    "strconv"
    "sync"
    "time"
)

// Order 任务结构体(模拟外卖订单)
type Order struct {
    ID       int
    Shop     string  // 店铺名
    Dish     string  // 菜品
    Distance float64 // 配送距离(km)
}

func GenOrderNo() string {
    now := time.Now().Unix()
    randNum := rand.Intn(10000)
    orderNo := fmt.Sprintf("%d%d", now, randNum)
    return orderNo
}

func main() {
    orderChan := make(chan Order, 5)

    var producerWg sync.WaitGroup
    var consumerWg sync.WaitGroup

    // 启动生产者(15个)
    for i := 1; i <= 15; i++ {
        producerWg.Add(1)
        go func(id int) {
            defer producerWg.Done()
            orderId, _ := strconv.ParseInt(GenOrderNo(), 10, 64)
            order := Order{
                ID:       int(orderId),
                Shop:     "厦门小吃店",
                Dish:     "海蛎煎",
                Distance: float64(9%3 + 1),
            }
            fmt.Printf("【生产者-%d】新订单:%d\n", id, order.ID)
            orderChan <- order
            time.Sleep(time.Millisecond * 10)
        }(i)
    }

    // 启动消费者(3个骑手)
    for i := 1; i <= 3; i++ {
        consumerWg.Add(1)
        go func(id int) {
            defer consumerWg.Done()
            for order := range orderChan {
                fmt.Printf("【骑手-%d】接到订单 %d,配送 %.1f km\n", id, order.ID, order.Distance)
                time.Sleep(time.Second * 5)
                fmt.Printf("【骑手-%d】订单 %d 已送达\n", id, order.ID)
            }
        }(i)
    }

    // 等待所有生产者完成 → 关闭 channel → 等待消费者完成
    producerWg.Wait()
    close(orderChan)
    consumerWg.Wait()

    fmt.Println("厦门外卖系统打烊啦~")
}

4. select 超时控制

package main

import (
    "context"
    "fmt"
    "time"
)

func callUserServe(ctx context.Context) (string, error) {
    timer := time.NewTimer(time.Second * 2)
    select {
    case <-timer.C:
        return "user_info", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func callOrderServe(ctx context.Context) (string, error) {
    timer := time.NewTimer(time.Second * 3)
    select {
    case <-timer.C:
        return "order_info", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    type result struct {
        name string
        data string
        err  error
    }
    resultCh := make(chan result, 2)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*4)
    defer cancel()

    go func() {
        data, err := callUserServe(ctx)
        resultCh <- result{"user", data, err}
    }()

    go func() {
        data, err := callOrderServe(ctx)
        resultCh <- result{"order", data, err}
    }()

    ret := make(map[string]string)
    for i := 0; i < 2; i++ {
        select {
        case <-ctx.Done(): // 整体超时或上游取消
            fmt.Println("整体超时或上游取消!")
            return
        case res := <-resultCh:
            if res.err != nil {
                fmt.Printf("%s error: %v\n", "事务", res.err)
                return
            }
            ret[res.name] = res.data
        }
    }
    fmt.Printf("最后结果: %v\n", ret)
}

5. Worker Pool(带最大并发数控制)

package main

import (
    "fmt"
    "sync"
    "time"
)

type Task func()

type WorkPool struct {
    maxWorkers int
    taskQueue  chan Task
    wg         sync.WaitGroup
    stopOnce   sync.Once
    stop       chan struct{}
}

func NewWorkPool(maxWorker int, queueSize int) *WorkPool {
    return &WorkPool{
        maxWorkers: maxWorker,
        taskQueue:  make(chan Task, queueSize),
        stop:       make(chan struct{}),
    }
}

func (p *WorkPool) worker() {
    defer p.wg.Done()
    for {
        select {
        case task, ok := <-p.taskQueue:
            if !ok {
                return // 任务队列已关闭,worker 退出
            }
            task()
        case <-p.stop:
            return // 收到停止信号,退出
        }
    }
}

func (p *WorkPool) Start() {
    for i := 0; i < p.maxWorkers; i++ {
        p.wg.Add(1)
        go p.worker()
    }
}

func (p *WorkPool) Submit(task Task) bool {
    select {
    case <-p.stop:
        return false
    default:
    }
    select {
    case p.taskQueue <- task:
        return true
    case <-p.stop:
        return false
    }
}

// StopGracefully 优雅关闭:不再接收新任务,等待所有已提交任务完成
func (p *WorkPool) StopGracefully() {
    p.stopOnce.Do(func() {
        close(p.taskQueue)
    })
    p.wg.Wait()
}

func main() {
    pool := NewWorkPool(2, 6) // 2个协程,队列长度6
    pool.Start()

    for i := 0; i < 10; i++ {
        taskID := i
        submit := pool.Submit(func() {
            fmt.Printf("task %d 开始\n", taskID)
            time.Sleep(500 * time.Millisecond)
            fmt.Printf("任务 %d 执行完毕\n", taskID)
        })
        if !submit {
            fmt.Printf("任务 %d 提交失败,池已停止\n", taskID)
        }
    }

    pool.StopGracefully()
    fmt.Println("pool stopped")
}

6. sync.Mutex 保护计数器

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.RWMutex
    value int
}

func (c *Counter) Add() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.value
}

func main() {
    var wg sync.WaitGroup
    c := &Counter{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add()
        }()
    }

    wg.Wait()
    fmt.Println(c.Value())
}

7. sync.RWMutex 实现读多写少的缓存

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.RWMutex
    value int
}

func (c *Counter) Add() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.value
}

func main() {
    var wg sync.WaitGroup
    c := &Counter{}

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Add()
        }()
    }

    wg.Wait()
    fmt.Println(c.Value())
}

Top ↑