- 响应格式示例对齐实际代码(http.Status 模式) - 补 6 大设计原则(YAGNI/DIP/SRP/OCP/ISP/KISS) - 补架构说明(分层+ISP,五边形架构) - 补重构原则:不向后兼容 + CMS 化 - 文件拆分决策矩阵(该拆/不该拆)
171 lines
6.5 KiB
Markdown
171 lines
6.5 KiB
Markdown
# Go 代码规范
|
||
|
||
## 文件大小
|
||
|
||
| 层次 | 单文件行数上限 | 说明 |
|
||
|---|---|---|
|
||
| Controller | ≤ 120 行 | 只管参数绑定 + 调用服务 + 返回 |
|
||
| Service | ≤ 300 行 | 承载业务逻辑,超过则拆子服务 |
|
||
| Repository | ≤ 200 行 | 纯数据库操作 |
|
||
| Model | ≤ 80 行 | 纯结构体 |
|
||
| Middleware | ≤ 60 行 | 单一职责 |
|
||
| Router | ≤ 60 行 | 只做路由映射 |
|
||
|
||
> **注意**:以上为建议上限,不是硬性指标。**是否拆分取决于职责是否内聚**,而非单纯看行数。拆分决策矩阵:
|
||
|
||
| 该拆 | 不该拆 |
|
||
|------|--------|
|
||
| 多个独立功能塞一个文件(如 7 个 Tab 的 JS 全混居 + 5 个 IIFE) | 页面内区域共享大量样式,拆了反而碎片化(如文章详情/列表共用 `article-card`) |
|
||
| 一个文件承担了压根不同域的工作(如所有 API 路由写一个文件) | 功能高度耦合,拆了互相 import 更乱(如评论增删查+@提及+上传一体) |
|
||
| CSS 随 HTML 模板拆分自然跟随 | 公共工具函数拆成碎片没有意义 |
|
||
| 路由文件按业务域自然分割(与 admin.go 对齐) | 文件超线但职责单一、内聚良好 — 维持现状 |
|
||
|
||
## 瘦控制器铁律
|
||
|
||
Controller 每个方法不超过三步:
|
||
|
||
1. 绑定请求参数
|
||
2. 调用 Service
|
||
3. 返回响应
|
||
|
||
```go
|
||
// ✅ 正确
|
||
func (c *PostController) Create(ctx *gin.Context) {
|
||
var req dto.CreatePostRequest
|
||
if err := ctx.ShouldBind(&req); err != nil {
|
||
common.Error(ctx, common.ErrInvalidParam, "参数错误")
|
||
return
|
||
}
|
||
post, err := c.postService.Create(ctx, req, getCurrentUser(ctx))
|
||
if err != nil {
|
||
common.Error(ctx, common.ErrInternal, "创建失败")
|
||
return
|
||
}
|
||
common.Ok(ctx, post)
|
||
}
|
||
|
||
// ❌ 错误:Controller 里写 if-else 业务分支
|
||
func (c *PostController) Create(ctx *gin.Context) {
|
||
// ... 参数绑定 ...
|
||
if req.Type == "draft" {
|
||
// 草稿逻辑写在这里 ↓ 错误!
|
||
post.Status = 0
|
||
} else {
|
||
// 发布逻辑写在这里 ↓ 错误!
|
||
post.Status = 1
|
||
if post.Score > 100 { ... }
|
||
}
|
||
}
|
||
```
|
||
|
||
业务判断全部放在 Service 层。
|
||
|
||
## Controller 禁止事项
|
||
|
||
- ❌ 直接调用 Repository
|
||
- ❌ 调用另一个 Controller
|
||
- ❌ 写 if-else 业务分支
|
||
- ❌ 在 handler 里直接操作数据库
|
||
- ❌ 代码回滚、重试等编排逻辑
|
||
|
||
## 命名规范
|
||
|
||
### 文件
|
||
|
||
- Go 文件:`snake_case.go`
|
||
- 模板文件:`snake_case.html`
|
||
- CSS/JS 文件:`kebab-case.css` / `camelCase.js`
|
||
|
||
### 结构体与方法
|
||
|
||
- Controller:`type PostController struct{}`,方法 `func (c *PostController) Create(...)`
|
||
- Service:`type PostService struct{ repo *PostRepo }`,方法 `func (s *PostService) Create(...)`
|
||
- Repository:`type PostRepo struct{ db *gorm.DB }`,方法 `func (r *PostRepo) FindByID(...)`
|
||
|
||
### 响应格式
|
||
|
||
统一使用 `common` 包的响应函数,HTTP 状态码直接传入:
|
||
|
||
```go
|
||
common.Ok(c, data) // 200 成功
|
||
common.OkWithMessage(c, data, "操作成功") // 200 带消息
|
||
common.OkMessage(c, "操作成功") // 200 纯消息
|
||
common.Error(c, http.StatusBadRequest, "参数错误") // 400
|
||
common.Error(c, http.StatusUnauthorized, "请先登录") // 401
|
||
common.Error(c, http.StatusForbidden, "权限不足") // 403
|
||
common.Error(c, http.StatusNotFound, "未找到") // 404
|
||
common.Error(c, http.StatusInternalServerError, "服务器错误") // 500
|
||
```
|
||
|
||
### 模板变量
|
||
|
||
使用驼峰命名,与模板文件保持一致:
|
||
|
||
```go
|
||
gin.H{
|
||
"Title": "页面标题",
|
||
"ExtraCSS": "/static/css/page.css",
|
||
"Guidelines": template.HTML(content), // HTML 内容需显式标记类型
|
||
}
|
||
```
|
||
|
||
## 依赖注入
|
||
|
||
使用构造器注入模式,Controller/Service/Middleware 的依赖通过构造函数传入:
|
||
|
||
```go
|
||
// 构造器注入依赖
|
||
func NewAuthController(authService authUseCase, sm *session.Manager, limiter rateLimiter, cfg *config.Config, siteSettings *config.SiteSettings) *AuthController {
|
||
return &AuthController{authService: authService, sessionManager: sm, rateLimiter: limiter, cfg: cfg, siteSettings: siteSettings}
|
||
}
|
||
```
|
||
|
||
Controller 层通过 **ISP 接口隔离** 声明最小依赖 —— 只声明自己需要的方法子集,不依赖完整 Service 接口:
|
||
|
||
```go
|
||
// controller/admin/interfaces.go
|
||
type siteSettingUseCase interface {
|
||
GetSettings() map[string]string
|
||
UpdateSetting(key, value string) error
|
||
UpdateBoolSetting(key string, value bool) error
|
||
GetAuditSettings(defaults *service.AuditDefaults) *service.AuditSettings
|
||
}
|
||
```
|
||
|
||
各 Controller 自身的 ISP 声明放在对应的 `interfaces.go` 文件中,严禁 controller 直接 import 完整的 Service 结构体。
|
||
|
||
## 设计原则
|
||
|
||
| 原则 | 含义 | 本项目实践 |
|
||
|------|------|-----------|
|
||
| **YAGNI** | You Ain't Gonna Need It — 不为可能的需求提前实现 | 分 Phase 推进,不做预留接口/未用抽象层 |
|
||
| **DIP**(依赖倒置) | 高层模块不依赖低层,都依赖抽象 | Controller → ISP 接口 ← Service;Service → Store 接口 ← Repository |
|
||
| **SRP**(单一职责) | 一个模块只为一个角色变更 | 拆分决策:「多域混居则拆,内聚共享则留」 |
|
||
| **OCP**(开闭原则) | 对扩展开放,对修改关闭 | `config.yaml` 角色/审核配置可扩展,不改代码 |
|
||
| **ISP**(接口隔离) | 不依赖用不到的接口 | Controller 只声明自己需要的方法子集,见下方 ISP 章节 |
|
||
| **KISS** | Keep It Simple | 连续字符校验纯函数、密码强度 switch-case,不做策略模式/DI 框架 |
|
||
|
||
### 架构风格
|
||
|
||
分层架构 + ISP 接口隔离,非纯六边形。Gin、GORM 等框架层不额外抽象,仅在业务边界通过接口倒置依赖方向:
|
||
|
||
```
|
||
Web 适配器 (Gin Controller) → ISP Ports (interfaces.go)
|
||
↓
|
||
Service 核心逻辑
|
||
↓
|
||
Store Interfaces (repository.go) ← DB 适配器 (GORM Repo)
|
||
```
|
||
|
||
## 重构原则
|
||
|
||
### 不向后兼容
|
||
|
||
本项目为全新项目,尚未发布正式版。**重构时直接废弃旧实现,不做兼容层**,不保留任何向后兼容代码。遇到旧代码直接删除/覆盖,不新增 `deprecated`、`legacy` 等标记或包装函数。
|
||
|
||
### CMS 化
|
||
|
||
所有面向最终用户的硬编码文本(品牌名、标语、邮箱、空状态提示、系统消息等)应纳入 `site_settings` 表配置化,而非硬编码在模板或 Go 代码中。管理后台需提供对应输入项。
|
||
|
||
例外:CSS 注释、JS debug 日志、代码中的技术常量不受此规则约束。
|