1
0
Files
nex/openspec/changes/backend-code-lint/design.md

131 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
后端项目使用 Go 1.26.2 开发,已集成 `golangci-lint v1.64.8` 作为 tool dependency`go.mod` 中声明),并通过 `make backend-lint` 调用。但当前**没有 `.golangci.yml` 配置文件**lint 以默认配置运行(仅启用极少量 linter且存在 embedfs 模块加载错误导致 lint 实际无法执行。
代码审计发现以下存量问题:
- 8 处 `err == sentinel` 应使用 `errors.Is()`
- 7 处 `_ = json.Marshal(...)` 忽略错误返回值
- 13 处 `zap.String("error", err.Error())` 应使用 `zap.Error(err)`
- 2 处 `fmt.Fprintf(os.Stderr, ...)` 应走 logger
- 1 处 `_ = io.ReadAll(...)` 忽略错误
## Goals / Non-Goals
**Goals:**
- 配置 golangci-lint将 README 中的编码规范转化为机器可检查的硬约束
- 引入 lefthook 实现 pre-commit 自动 lintAI 提交代码时自动拦截违规
- 修复存量代码中的规范违规
- 解决 embedfs 导致 lint 无法运行的阻塞问题
**Non-Goals:**
- 不引入自定义 linter 插件(开发成本过高)
- 不配置 CI pipeline lint 门禁(仅本地)
- 不改变现有错误响应策略(允许 err.Error() 暴露在 HTTP 响应中)
- 不引入 funlen lintergocyclo 已控制复杂度funlen 误报率高)
- 不引入 unparam linter项目 interface 密集unparam 误报率高)
## Decisions
### D1: Linter 选型 — 13 个 linter 分四层
```
🔒 硬约束层(项目规范 → 机器检查)
├── forbidigo 禁止 fmt.Print*/log.*/zap.L()/zap.S()
├── errorlint 强制 errors.Is/As禁止 err == 比较
└── errcheck 禁止忽略错误返回值check-blank: true
🏗️ 质量基线层
├── staticcheck Go 团队官方综合静态分析
├── revive golint 替代品,精选 8 条规则
│ (exported, var-naming, indent-error-flow, error-strings,
│ error-return, blank-imports, context-as-argument,
│ unexported-return)
├── gocritic 100+ 代码质量规则
└── gosec 安全检查
🛡️ 资源安全层
├── bodyclose HTTP 响应 Body 关闭检查
├── noctx HTTP 请求必须携带 context
└── nilerr 检查 if err != nil { return nil } 遗漏
📐 格式层(可自动修复)
├── gofumpt gofmt 的严格版
└── goimports import 排序local-prefixes: nex/backend
📊 复杂度
└── gocyclo 正式代码 ≤10 / 测试代码 ≤20
```
**替代方案**: 使用 revive 自定义规则检查 zap.String("error", err.Error()) → 决定暂不实施,先修复存量 13 处 + README 约定 + code review如果后续频繁违规再投入开发自定义规则。
### D2: forbidigo 配置策略
禁止列表:
- `fmt\.Print*` — 必须使用 zap logger
- `fmt\.Fprint*(os\.(Stdout|Stderr)` — 必须使用 zap logger
- `log\.(Print|Fatal|Panic|Printf)*` — 必须使用 zap logger
- `zap\.L()` — 必须通过 DI 注入 *zap.Logger
- `zap\.S()` — 不使用 Sugar logger
**不禁止**
- `zap.Logger.Fatal()` — main() 中的 Fatal 是合理的启动终止模式,且 forbidigo 按函数匹配不会拦截 zap.Logger 的方法调用
- `fmt.Sprintf` — 格式化字符串是合法用途
- `fmt.Errorf` — 创建带格式的错误是标准用法
### D3: 测试代码 vs 正式代码差异化规则
| 规则 | 正式代码 | 测试代码 |
|---|---|---|
| forbidigo | 全部启用 | 放宽fmt.Sprintf 等合理) |
| errcheck | check-blank: true | 放宽 check-blank |
| revive (exported) | 启用 | 排除 |
| gosec | 启用 | 排除 G101/G401/G501 |
| gocyclo | ≤10 | ≤20 |
通过 `issues.exclude-rules` 按路径 `*_test.go``tests/` 配置排除。
### D4: 生成代码排除
`tests/mocks/` 目录下的 8 个 mock 文件由 mockgen 生成,需要排除。使用 `issues.exclude-dirs` 配合 `issues.exclude-generated: true`golangci-lint 自动检测 `Code generated by` 标记)。
### D5: embedfs 阻塞问题修复
embedfs 模块使用 `//go:embed assets/*``//go:embed frontend-dist/*`,但这些目录在未构建时不存在,导致 golangci-lint 无法加载该模块。
**方案**: 在 `embedfs/assets/``embedfs/frontend-dist/` 中添加 `.gitkeep` 文件,使 `go:embed` 指令能匹配到内容。这是最小侵入的解决方案不影响正常构建流程desktop-build 会覆盖这些目录)。
### D6: lefthook pre-commit 配置
```yaml
# lefthook.yml
pre-commit:
commands:
backend-lint:
glob: "backend/**/*.go"
run: cd backend && go tool golangci-lint run --new-from-rev HEAD {staged_files}
```
关键设计:
- 只检查 staged 文件(`--new-from-rev HEAD`),速度快
- 只在 Go 文件变更时触发(`glob: "backend/**/*.go"`
- AI commit 时自动触发lint 不过则 commit 被拒绝,形成自动反馈循环
### D7: 存量修复策略
按优先级分批修复:
1. **P0 — embedfs 阻塞修复**:创建 .gitkeep 文件
2. **P1 — err == sentinel → errors.Is()**8 处,分布在 handler 和 client
3. **P2 — 忽略错误返回值**7 处 json.Marshal + 1 处 io.ReadAll + 2 处 stats Record加 nolint 注释)
4. **P3 — zap.String("error", err.Error()) → zap.Error(err)**13 处
5. **P4 — fmt.Fprintf(os.Stderr) → logger**2 处,在 cmd/desktop/
## Risks / Trade-offs
**[lint 速度影响 commit 体验]** → lefthook 只检查 staged Go 文件,增量检查通常 <5 秒,可接受。如果仍慢,可加 `--timeout` 限制。
**[lefthook 是新依赖]** → lefthook 是开发工具依赖,不影响生产代码。作为单二进制分发,安装简单(`go install` 或从 GitHub release 下载)。首次需要开发者手动安装。
**[存量修复可能引入新 bug]** → 所有修复都是机械性替换errors.Is、zap.Error 等),不改变逻辑。修复后运行 `make test` 确认无回归。
**[forbidigo 可能误拦合理用法]** → 通过仔细配置允许列表(允许 fmt.Sprintf、fmt.Errorf并在发现误报时调整规则。