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

6.0 KiB
Raw Blame History

Context

后端项目使用 Go 1.26.2 开发,已集成 golangci-lint v1.64.8 作为 tool dependencygo.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.gotests/ 配置排除。

D4: 生成代码排除

tests/mocks/ 目录下的 8 个 mock 文件由 mockgen 生成,需要排除。使用 issues.exclude-dirs 配合 issues.exclude-generated: truegolangci-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 配置

# 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) → logger2 处,在 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并在发现误报时调整规则。