go-sapcc-conventions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSAP Converged Cloud Go Conventions
SAP Converged Cloud Go编码规范
Coding standards extracted from extensive PR review analysis across and . These are the real rules enforced in code review by the project's lead review standards.
sapcc/keppelsapcc/go-bits本编码标准源自对和大量PR评审的分析,是项目主评审实际强制执行的规则。
sapcc/keppelsapcc/go-bitsTool Integration
工具集成
gopls MCP (MUST use when available): Use at session start, after reading .go files, before modifying any symbol (critical for sapcc — lead review checks cross-package impact), after every edit, after go.mod changes. This gives type-aware analysis that catches issues grep cannot.
go_workspacego_file_contextgo_symbol_referencesgo_diagnosticsgo_vulncheckModern Go Guidelines: Detect Go version from go.mod. Sapcc projects typically target Go 1.22+. Use version-appropriate features: (1.24+), (1.24+), (1.24+), (1.25+), (1.26+).
t.Context()b.Loop()strings.SplitSeqwg.Go()errors.AsType[T]gopls MCP(可用时必须使用):会话启动时使用,读取.go文件后使用,修改任何符号前使用(这对sapcc至关重要——主评审会检查跨包影响),每次编辑后使用,修改go.mod后使用。这能提供类型感知分析,捕获grep无法发现的问题。
go_workspacego_file_contextgo_symbol_referencesgo_diagnosticsgo_vulncheck现代Go指南:从go.mod中检测Go版本。Sapcc项目通常以Go 1.22+为目标版本,使用对应版本的特性:(1.24+)、(1.24+)、(1.24+)、(1.25+)、(1.26+)。
t.Context()b.Loop()strings.SplitSeqwg.Go()errors.AsType[T]1. Anti-Over-Engineering Rules (Strongest Project Opinions)
1. 反过度工程规则(项目核心主张)
This section comes first because it is the defining characteristic of SAP CC Go code. The most frequent review theme (10 of 38 comments) is rejecting unnecessary complexity.
本部分放在最前面,因为它是SAP CC Go代码的标志性特征。评审中最常见的主题(38条评论中有10条)就是拒绝不必要的复杂性。
1.1 When NOT to Create Types
1.1 何时不应创建类型
Do not create throwaway struct types just to marshal a simple JSON payload:
go
// BAD: Copilot suggested this. Rejected as "overengineered."
type fsParams struct { Path string `json:"path"` }
type fsConfig struct { Type string `json:"type"`; Params fsParams `json:"params"` }
config, _ := json.Marshal(fsConfig{Type: "filesystem", Params: fsParams{Path: path}})
// GOOD: project convention
storageConfig = fmt.Sprintf(`{"type":"filesystem","params":{"path":%s}}`,
must.Return(json.Marshal(filesystemPath)))不要只为了序列化简单的JSON负载而创建一次性结构体类型:
go
// 错误示例:Copilot建议的写法,被判定为“过度工程”
type fsParams struct { Path string `json:"path"` }
type fsConfig struct { Type string `json:"type"`; Params fsParams `json:"params"` }
config, _ := json.Marshal(fsConfig{Type: "filesystem", Params: fsParams{Path: path}})
// 正确示例:项目约定写法
storageConfig = fmt.Sprintf(`{"type":"filesystem","params":{"path":%s}}`,
must.Return(json.Marshal(filesystemPath)))1.2 When NOT to Wrap Errors
1.2 何时不应包装错误
Do not add error context that the called function already provides. The Go stdlib's functions include the function name, input value, and error reason:
strconvgo
// BAD: redundant wrapping
val, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return fmt.Errorf("failed to parse chunk number %q: %w", s, err)
}
// GOOD: strconv already says "strconv.ParseUint: parsing \"hello\": invalid syntax"
chunkNumber := must.Return(strconv.ParseUint(chunkNumberStr, 10, 32))"ParseUint is disciplined about providing good context in its input messages... So we can avoid boilerplate here without compromising that much clarity."
不要添加被调用函数已提供的错误上下文。Go标准库的函数会包含函数名、输入值和错误原因:
strconvgo
// 错误示例:冗余包装
val, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return fmt.Errorf("failed to parse chunk number %q: %w", s, err)
}
// 正确示例:strconv已包含足够上下文,例如"strconv.ParseUint: parsing \"hello\": invalid syntax"
chunkNumber := must.Return(strconv.ParseUint(chunkNumberStr, 10, 32))"ParseUint会规范地在输入消息中提供良好的上下文...因此我们可以在此处避免样板代码,同时不会显著降低清晰度。"
1.3 When NOT to Handle Errors
1.3 何时不应处理错误
Do not handle errors that are never triggered in practice. Apply the standard consistently:
go
// BAD: handling os.Stdout.Write errors
n, err := os.Stdout.Write(data)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
// GOOD: if fmt.Println ignoring errors is OK everywhere, so is os.Stdout.Write
os.Stdout.Write(data)"I'm going to ignore this based purely on the fact that Copilot complains about, but not about the much more numerous instances ofos.Stdout.Write()that theoretically suffer the same problem."fmt.Println
不要处理实际中永远不会触发的错误。请始终如一地遵循此标准:
go
// 错误示例:处理os.Stdout.Write错误
n, err := os.Stdout.Write(data)
if err != nil {
return fmt.Errorf("failed to write to stdout: %w", err)
}
// 正确示例:如果fmt.Println可以忽略错误,那么os.Stdout.Write也可以
os.Stdout.Write(data)"我决定忽略这个问题,因为Copilot会抱怨,但不会抱怨大量理论上存在相同问题的os.Stdout.Write()调用。"fmt.Println
1.4 When NOT to Add Defer Close
1.4 何时不应添加Defer Close
Do not add on just for theoretical contract compliance:
defer Close()io.NopCloser"This is an irrelevant contrivance. Eitherdoes it, or the operation fails and we fatal-error out, in which case it does not matter anyway."WriteTrivyReport
不要为添加,仅仅为了理论上的契约合规:
io.NopCloserdefer Close()"这是无关紧要的矫揉造作。要么会处理它,要么操作失败并触发致命错误,此时处理它也无关紧要。"WriteTrivyReport
1.5 Dismiss Copilot/AI Suggestions That Add Complexity
1.5 拒绝增加复杂度的Copilot/AI建议
Lead review evaluates AI suggestions on merit and frequently simplifies them:
- If a Copilot suggestion is inconsistent (complains about X but not equivalent Y), dismiss it
- If a Copilot suggestion creates types for one-off marshaling, simplify it
- Ask: "Can you point to a concrete scenario where this fails?" If not, don't handle it
主评审会根据实际价值评估AI建议,并经常简化它们:
- 如果Copilot的建议不一致(抱怨X但不抱怨等价的Y),则拒绝它
- 如果Copilot的建议为一次性序列化创建类型,则简化它
- 问自己:“你能指出这个方案会失败的具体场景吗?”如果不能,就不要处理它
1.6 When NOT to Build Smart Inference
1.6 何时不应构建智能推断
When a known future design change is coming, don't build abstractions that will break:
undefined当已知未来会有设计变更时,不要构建会被打破的抽象:
undefinedBAD: inferring params from driver name (won't work for future "multi" driver)
错误示例:从驱动名称推断参数(对未来的"multi"驱动无效)
--driver swift (auto-generates swift params)
--driver swift (自动生成swift参数)
GOOD: explicit and future-proof
正确示例:显式且面向未来
--driver swift --params '{"container":"foo"}'
> "I appreciate the logic behind inferring storage driver params automatically... But this will not scale beyond next month."
But also: do NOT preemptively solve the future problem. Just don't build something that blocks the future solution.--driver swift --params '{"container":"foo"}'
> "我理解自动推断存储驱动参数的逻辑...但这种方式撑不过下个月。"
同时:**不要预先解决未来的问题**。只需确保当前实现不会阻碍未来的解决方案。1.7 No Hidden Defaults for Niche Cases
1.7 niche场景不要使用隐藏默认值
If a default value only applies to a subset of use cases, make the parameter required for everyone:
"This is, in effect, a default value that only applies to two specific storage drivers. These are not widely used enough to justify the undocumented quirk."
如果默认值仅适用于部分用例,那么要求所有用户都显式指定该参数:
"这实际上是一个仅适用于两个特定存储驱动的默认值。它们的使用范围不够广泛,不足以证明这种未记录的特殊处理是合理的。"
2. Lead Review Rules
2. 主评审规则
The lead review style is directive. Statements, not suggestions. Top concerns: simplicity, API design, error handling. See references/review-standards-lead.md for all 21 PR comments with full context.
主评审的风格是指令式的,直接陈述而非建议。最关注的点是:简洁性、API设计、错误处理。查看references/review-standards-lead.md获取所有21条PR评论的完整上下文。
Core Principles
核心原则
| Rule | Summary |
|---|---|
| Trust the stdlib | Don't wrap errors that |
| Use Cobra subcommands | Never manually roll argument dispatch that Cobra handles |
| CLI names: specific + extensible | |
| Marshal structured data for errors | If you have a |
| Tests must verify behavior | Never silently remove test assertions during refactoring |
| Explain test workarounds | Add comments when test setup diverges from production patterns |
| Use existing error utilities | Use |
| TODOs need context | Include what, a starting point link, and why not done now |
| Documentation stays qualified | When behavior changes conditionally, update docs to state the conditions |
| Understand value semantics | Value receiver copies the struct, but reference-type fields share data |
| Variable names don't mislead | Don't name script vars as if the application reads them |
| 规则 | 摘要 |
|---|---|
| 信任标准库 | 不要包装 |
| 使用Cobra子命令 | 永远不要手动实现Cobra已能处理的参数分发 |
| CLI命名:具体且可扩展 | 使用 |
| 为错误序列化结构化数据 | 如果有 |
| 测试必须验证行为 | 重构时永远不要静默移除测试断言 |
| 解释测试变通方案 | 当测试设置与生产模式不同时,添加注释说明 |
| 使用现有错误工具 | 使用 |
| TODO需要上下文 | 包含要做的事、起始链接以及当前未完成的原因 |
| 文档保持限定性 | 当行为有条件地变化时,更新文档说明条件 |
| 理解值语义 | 值接收器会复制结构体,但引用类型字段会共享数据 |
| 变量名不要误导 | 不要将脚本变量命名为应用程序会读取的名称 |
How Lead Review Works
主评审工作方式
- Reads Copilot suggestions critically -- agrees with principle, proposes simpler alternatives
- Dismisses inconsistent AI complaints -- if tool flags X but not equivalent Y, the concern is invalid
- Thinks about forward compatibility -- command names and API shapes evaluated for extensibility
- Values brevity when stdlib provides clarity -- removes wrappers that duplicate error info
- Approves simple PRs quickly -- doesn't manufacture concerns
- Corrects misconceptions directly -- states correct behavior without softening
- Pushes fixes directly -- sometimes pushes commits to address review concerns directly
- 批判性地阅读Copilot建议——认同原则,但提出更简单的替代方案
- 拒绝不一致的AI抱怨——如果工具标记X但不标记等价的Y,则该问题无效
- 考虑向前兼容性——评估命令名和API形状的可扩展性
- 当标准库提供清晰度时,追求简洁——移除重复错误信息的包装
- 快速批准简单的PR——不要无中生有地提出问题
- 直接纠正误解——陈述正确行为,不做软化处理
- 直接推送修复——有时会直接提交代码来解决评审问题
3. Secondary Review Rules
3. 次级评审规则
The secondary review style is inquisitive. Questions where lead review makes statements. Top concerns: configuration safety, migration paths, test completeness. See references/review-standards-secondary.md for full details.
次级评审的风格是探究式的,主评审陈述规则,次级评审则提出问题。最关注的点是:配置安全性、迁移路径、测试完整性。查看references/review-standards-secondary.md获取完整详情。
Core Principles
核心原则
| Rule | Summary |
|---|---|
| Error messages must be actionable | "Internal Server Error" is unacceptable when the cause is knowable |
| Know the spec, deviate pragmatically | Reference RFCs, but deviate when spec is impractical |
| Guard against panics with clear errors | Check nil/empty before indexing, use |
| Strict configuration parsing | Use |
| Test ALL combinations | When changing logic with multiple inputs, test every meaningful combination |
| Eliminate redundant code | Ask "This check is now redundant?" when code is refactored |
| Comments explain WHY | When something non-obvious is added, request an explanatory comment |
| Domain knowledge over theory | Dismiss concerns that don't apply to actual domain constraints |
| Smallest possible fix | 2-line PRs are fine. Don't bundle unrelated changes |
| Respect ownership hierarchy | "LGTM but lets wait for lead review, we are in no hurry here" |
| Be honest about mistakes | Acknowledge errors quickly and propose fix direction |
| Validate migration paths | "Do we somehow check if this is still set and then abort?" |
| 规则 | 摘要 |
|---|---|
| 错误消息必须可操作 | 当原因可知时,“Internal Server Error”是不可接受的 |
| 了解规范,务实偏离 | 参考RFC,但当规范不切实际时可以偏离 |
| 用清晰的错误防止panic | 在索引前检查nil/空值,使用 |
| 严格的配置解析 | 对JSON解码器使用 |
| 测试所有组合 | 当修改有多个输入的逻辑时,测试所有有意义的组合 |
| 消除冗余代码 | 重构时问自己“这个检查现在是不是冗余了?” |
| 注释解释原因 | 当添加非显而易见的内容时,需要添加解释性注释 |
| 领域知识优先于理论 | 拒绝不适用于实际领域约束的问题 |
| 尽可能最小的修复 | 2行代码的PR是可以的。不要捆绑无关变更 |
| 尊重所有权层级 | “我已批准,但请等主评审确认,我们不着急” |
| 坦诚承认错误 | 快速承认错误并提出修复方向 |
| 验证迁移路径 | “我们是否需要检查这个设置是否仍然存在,然后中止?” |
4. Architecture Rules
4. 架构规则
Keppel uses a strict layered architecture. See references/architecture-patterns.md for the complete 102-rule set with code examples.
Keppel使用严格的分层架构。查看references/architecture-patterns.md获取包含代码示例的完整102条规则集。
Directory Structure
目录结构
project/
main.go # Root: assembles Cobra commands, blank-imports drivers
cmd/<component>/main.go # AddCommandTo(parent *cobra.Command) pattern
internal/
api/<surface>/ # HTTP handlers per API surface (keppelv1, registryv2)
auth/ # Authorization logic
client/ # Outbound HTTP clients
drivers/<name>/ # Pluggable driver implementations (register via init())
keppel/ # Core domain: interfaces, config, DB, errors
models/ # DB model structs (pure data, db: tags, no logic)
processor/ # Business logic (coordinates DB + storage)
tasks/ # Background jobs
test/ # Test infrastructure, doubles, helpersproject/
main.go # 根目录:组装Cobra命令,空白导入驱动
cmd/<component>/main.go # 使用AddCommandTo(parent *cobra.Command)模式
internal/
api/<surface>/ # 每个API表面的HTTP处理器(keppelv1、registryv2)
auth/ # 授权逻辑
client/ # 出站HTTP客户端
drivers/<name>/ # 可插拔驱动实现(通过init()注册)
keppel/ # 核心领域:接口、配置、数据库、错误
models/ # 数据库模型结构体(纯数据,含db:标签,无逻辑)
processor/ # 业务逻辑(协调数据库+存储)
tasks/ # 后台任务
test/ # 测试基础设施、替身、助手Key Patterns
关键模式
Pluggable Driver Pattern (6 driver types in keppel):
go
// Interface in internal/keppel/
type StorageDriver interface {
pluggable.Plugin // requires PluginTypeID() string
Init(...) error
// domain methods...
}
var StorageDriverRegistry pluggable.Registry[StorageDriver]
// Implementation in internal/drivers/<name>/
func init() {
keppel.StorageDriverRegistry.Add(func() keppel.StorageDriver { return &myDriver{} })
}
// Activation in main.go
_ "github.com/sapcc/keppel/internal/drivers/openstack"Cobra Command Pattern:
go
// cmd/<name>/main.go
package apicmd
func AddCommandTo(parent *cobra.Command) {
cmd := &cobra.Command{Use: "api", Short: "...", Args: cobra.NoArgs, Run: run}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) {
keppel.SetTaskName("api")
cfg := keppel.ParseConfiguration()
ctx := httpext.ContextWithSIGINT(cmd.Context(), 10*time.Second)
// ... bootstrap drivers, DB, handlers ...
}Configuration: Environment variables only. No config files, no CLI flags for config. JSON params only for driver internals.
go
// Required vars
host := osext.MustGetenv("KEPPEL_API_PUBLIC_FQDN")
// Optional with defaults
port := osext.GetenvOrDefault("KEPPEL_DB_PORT", "5432")
// Boolean flags
debug := osext.GetenvBool("KEPPEL_DEBUG")可插拔驱动模式(Keppel中有6种驱动类型):
go
// internal/keppel/中的接口
type StorageDriver interface {
pluggable.Plugin // 要求实现PluginTypeID() string
Init(...) error
// 领域方法...
}
var StorageDriverRegistry pluggable.Registry[StorageDriver]
// internal/drivers/<name>/中的实现
func init() {
keppel.StorageDriverRegistry.Add(func() keppel.StorageDriver { return &myDriver{} })
}
// main.go中的激活
_ "github.com/sapcc/keppel/internal/drivers/openstack"Cobra命令模式:
go
// cmd/<name>/main.go
package apicmd
func AddCommandTo(parent *cobra.Command) {
cmd := &cobra.Command{Use: "api", Short: "...", Args: cobra.NoArgs, Run: run}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) {
keppel.SetTaskName("api")
cfg := keppel.ParseConfiguration()
ctx := httpext.ContextWithSIGINT(cmd.Context(), 10*time.Second)
// ... 引导驱动、数据库、处理器 ...
}配置:仅使用环境变量。无配置文件,无用于配置的CLI标志。仅驱动内部使用JSON参数。
go
// 必填变量
host := osext.MustGetenv("KEPPEL_API_PUBLIC_FQDN")
// 可选变量带默认值
port := osext.GetenvOrDefault("KEPPEL_DB_PORT", "5432")
// 布尔标志
debug := osext.GetenvBool("KEPPEL_DEBUG")5. Error Handling Rules
5. 错误处理规则
See references/architecture-patterns.md for the complete 27-rule error handling specification.
查看references/architecture-patterns.md获取包含27条规则的完整错误处理规范。
Error Wrapping Conventions
错误包装约定
go
// "while" for operations in progress
return fmt.Errorf("while finding source repository: %w", err)
// "cannot" for failed actions
return fmt.Errorf("cannot parse digest %q: %w", digestStr, err)
// "during" for HTTP operations
return fmt.Errorf("during %s %s: %w", r.Method, uri, err)
// "could not" for background jobs
return fmt.Errorf("could not get ManagedAccountNames(): %w", err)All error messages: lowercase, no trailing punctuation, include identifying data with , descriptive action prefix.
%qgo
// "while"用于进行中的操作
return fmt.Errorf("while finding source repository: %w", err)
// "cannot"用于失败的操作
return fmt.Errorf("cannot parse digest %q: %w", digestStr, err)
// "during"用于HTTP操作
return fmt.Errorf("during %s %s: %w", r.Method, uri, err)
// "could not"用于后台任务
return fmt.Errorf("could not get ManagedAccountNames(): %w", err)所有错误消息:小写,无结尾标点,使用包含标识数据,带描述性动作前缀。
%qmust.Return / must.Succeed Scope
must.Return / must.Succeed的适用范围
go
// ALLOWED: startup/bootstrap code (fatal errors)
must.Succeed(rootCmd.Execute())
var celEnv = must.Return(cel.NewEnv(...))
// ALLOWED: test code
must.SucceedT(t, s.DB.Insert(&record))
digest := must.ReturnT(rc.UploadBlob(ctx, data))(t)
// FORBIDDEN: request handlers, business logic, background tasks
// Never use must.* where errors should be propagatedgo
// 允许:启动/引导代码(致命错误)
must.Succeed(rootCmd.Execute())
var celEnv = must.Return(cel.NewEnv(...))
// 允许:测试代码
must.SucceedT(t, s.DB.Insert(&record))
digest := must.ReturnT(rc.UploadBlob(ctx, data))(t)
// 禁止:请求处理器、业务逻辑、后台任务
// 永远不要在需要传播错误的地方使用must.*must vs assert in Tests: When to Use Which
测试中must与assert的使用场景
In test code, and serve different roles:
mustassert| Package | Calls | Use When |
|---|---|---|
| | Checking the expected outcome of the operation being tested |
| | Setup/preconditions where failure means subsequent lines are meaningless |
Decision tree:
- Inside a helper? ->
mustXxx/must.SucceedTmust.ReturnT - Next line depends on this succeeding? -> /
must.SucceedTmust.ReturnT - Checking the outcome of the tested operation? ->
assert.ErrEqual(t, err, nil) - Need a return value? -> (no assert equivalent)
must.ReturnT
go
// Setup (fatal) — next lines depend on this
must.SucceedT(t, store.UpdateMetrics())
families := must.ReturnT(registry.Gather())(t)
// Assertion (non-fatal) — checking expected outcome
assert.ErrEqual(t, err, nil)
assert.Equal(t, len(families), 3)The rule: helper = must, assertion = assert.
在测试代码中,和有不同的作用:
mustassert| 包 | 调用方式 | 使用场景 |
|---|---|---|
| | 检查被测试操作的预期结果 |
| | 设置/前置条件,失败意味着后续代码无意义 |
决策树:
- 在助手函数中? -> 使用
mustXxx/must.SucceedTmust.ReturnT - 下一行代码依赖此操作成功? -> 使用/
must.SucceedTmust.ReturnT - 检查被测试操作的结果? -> 使用
assert.ErrEqual(t, err, nil) - 需要返回值? -> 使用(assert无等价方法)
must.ReturnT
go
// 设置(致命)——后续代码依赖此操作
must.SucceedT(t, store.UpdateMetrics())
families := must.ReturnT(registry.Gather())(t)
// 断言(非致命)——检查预期结果
assert.ErrEqual(t, err, nil)
assert.Equal(t, len(families), 3)规则:助手函数用must,断言用assert。
assert.Equal vs assert.DeepEqual
assert.Equal与assert.DeepEqual的选择
Type supports | Use | Args |
|---|---|---|
| Yes (int, string, bool) | | 3 |
| No (slices, maps, structs) | | 4 |
Common mistake flagged in review: — returns which is comparable, so use .
assert.DeepEqual(t, "count", len(events), 3)lenintassert.Equal(t, len(events), 3)类型支持 | 使用方法 | 参数 |
|---|---|---|
| 是(int、string、bool) | | 3个 |
| 否(切片、映射、结构体) | | 4个 |
评审中常见的错误: —— 返回的是可比较的,应使用。
assert.DeepEqual(t, "count", len(events), 3)lenintassert.Equal(t, len(events), 3)Logging Level Selection
日志级别选择
| Level | When | Example |
|---|---|---|
| Startup/CLI only, never in handlers | |
| Cannot bubble up (cleanup, deferred, advisory) | |
| Operational events, graceful degradation | |
| Diagnostic, gated behind | |
| 级别 | 使用场景 | 示例 |
|---|---|---|
| 仅用于启动/CLI,永远不要在处理器中使用 | |
| 无法向上传播的错误(清理、延迟操作、通知) | |
| 操作事件、优雅降级 | |
| 诊断信息,受 | |
Panic Rules
Panic规则
Panic ONLY for:
- Programming errors / unreachable code:
panic("unreachable") - Invariant violations:
panic("(why was this not caught by Validate!?)") - Infallible operations: ,
crypto/rand.Readon known-good datajson.Marshal - Init-order violations:
panic("called before Connect()")
NEVER panic for: user input, external services, database errors, request handling.
仅在以下情况使用Panic:
- 编程错误/不可达代码:
panic("unreachable") - 不变量违反:
panic("(为什么Validate没有捕获这个问题?!)") - 不可能失败的操作:、对已知有效数据的
crypto/rand.Readjson.Marshal - 初始化顺序违反:
panic("called before Connect()")
永远不要在以下场景使用Panic:用户输入、外部服务、数据库错误、请求处理。
HTTP Error Response Formats (3 distinct)
HTTP错误响应格式(3种不同格式)
| API Surface | Format | Helper |
|---|---|---|
Registry V2 ( | JSON | |
Keppel V1 ( | Obfuscated text (5xx get UUID-masked) | |
Auth ( | JSON | |
5xx errors use which logs the real error with a UUID and returns to the client.
respondwith.ObfuscatedErrorText"Internal Server Error (ID = <uuid>)"| API表面 | 格式 | 助手函数 |
|---|---|---|
Registry V2 ( | JSON | |
Keppel V1 ( | 模糊文本(5xx错误会被UUID掩码) | |
Auth ( | JSON | |
5xx错误使用,它会用UUID记录真实错误,并向客户端返回。
respondwith.ObfuscatedErrorText"Internal Server Error (ID = <uuid>)"6. API Design Rules
6. API设计规则
Handler Pattern (Every handler follows this sequence)
处理器模式(所有处理器遵循此流程)
go
func (a *API) handleGetAccount(w http.ResponseWriter, r *http.Request) {
// 1. ALWAYS first: identify for metrics
httpapi.IdentifyEndpoint(r, "/keppel/v1/accounts/:account")
// 2. Authenticate BEFORE any data access
authz := a.authenticateRequest(w, r, accountScopeFromRequest(r, keppel.CanViewAccount))
if authz == nil {
return // error already written
}
// 3. Load resources
account := a.findAccountFromRequest(w, r, authz)
if account == nil {
return // error already written
}
// 4. Business logic...
// 5. Respond
respondwith.JSON(w, http.StatusOK, map[string]any{"account": rendered})
}go
func (a *API) handleGetAccount(w http.ResponseWriter, r *http.Request) {
// 1. 始终首先执行:为指标标识端点
httpapi.IdentifyEndpoint(r, "/keppel/v1/accounts/:account")
// 2. 在任何数据访问之前进行认证
authz := a.authenticateRequest(w, r, accountScopeFromRequest(r, keppel.CanViewAccount))
if authz == nil {
return // 错误已写入响应
}
// 3. 加载资源
account := a.findAccountFromRequest(w, r, authz)
if account == nil {
return // 错误已写入响应
}
// 4. 业务逻辑...
// 5. 响应
respondwith.JSON(w, http.StatusOK, map[string]any{"account": rendered})
}Strict JSON Parsing
严格JSON解析
go
// ALWAYS use DisallowUnknownFields for request bodies
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&req)
if err != nil {
http.Error(w, "request body is not valid JSON: "+err.Error(), http.StatusBadRequest)
return
}go
// 始终对请求体使用DisallowUnknownFields
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&req)
if err != nil {
http.Error(w, "request body is not valid JSON: "+err.Error(), http.StatusBadRequest)
return
}Response Conventions
响应约定
go
// Single resource: wrap in named key
respondwith.JSON(w, http.StatusOK, map[string]any{"account": rendered})
// Collection: wrap in plural named key
respondwith.JSON(w, http.StatusOK, map[string]any{"accounts": list})
// Empty list: MUST be [], never null
if len(items) == 0 {
items = []ItemType{}
}go
// 单个资源:包装在命名键中
respondwith.JSON(w, http.StatusOK, map[string]any{"account": rendered})
// 集合:包装在复数命名键中
respondwith.JSON(w, http.StatusOK, map[string]any{"accounts": list})
// 空列表:必须是[],绝不能是null
if len(items) == 0 {
items = []ItemType{}
}7. Testing Rules
7. 测试规则
Core Testing Stack
核心测试栈
- Assertion library: (NOT testify, NOT gomock)
go-bits/assert - DB testing: in every
easypg.WithTestDBTestMain - Test setup: Functional options via
test.NewSetup(t, ...options) - HTTP testing:
assert.HTTPRequest{}.Check(t, handler) - Time control: (never call
mock.Clockdirectly)time.Now() - Test doubles: Implement real driver interfaces, register via
init()
- 断言库:(不要使用testify,不要使用gomock)
go-bits/assert - 数据库测试:每个中使用
TestMaineasypg.WithTestDB - 测试设置:通过使用函数式选项
test.NewSetup(t, ...options) - HTTP测试:
assert.HTTPRequest{}.Check(t, handler) - 时间控制:(永远不要直接调用
mock.Clock)time.Now() - 测试替身:实现真实驱动接口,通过注册
init()
assert.HTTPRequest Pattern
assert.HTTPRequest模式
go
assert.HTTPRequest{
Method: "PUT",
Path: "/keppel/v1/accounts/first",
Header: map[string]string{"X-Test-Perms": "change:tenant1"},
Body: assert.JSONObject{
"account": assert.JSONObject{"auth_tenant_id": "tenant1"},
},
ExpectStatus: http.StatusOK,
ExpectHeader: map[string]string{
test.VersionHeaderKey: test.VersionHeaderValue,
},
ExpectBody: assert.JSONObject{
"account": assert.JSONObject{
"name": "first", "auth_tenant_id": "tenant1",
},
},
}.Check(t, h)go
assert.HTTPRequest{
Method: "PUT",
Path: "/keppel/v1/accounts/first",
Header: map[string]string{"X-Test-Perms": "change:tenant1"},
Body: assert.JSONObject{
"account": assert.JSONObject{"auth_tenant_id": "tenant1"},
},
ExpectStatus: http.StatusOK,
ExpectHeader: map[string]string{
test.VersionHeaderKey: test.VersionHeaderValue,
},
ExpectBody: assert.JSONObject{
"account": assert.JSONObject{
"name": "first", "auth_tenant_id": "tenant1",
},
},
}.Check(t, h)DB Testing Pattern
数据库测试模式
go
// In shared_test.go -- REQUIRED for every package with DB tests
func TestMain(m *testing.M) {
easypg.WithTestDB(m, func() int { return m.Run() })
}
// Full DB snapshot assertion
easypg.AssertDBContent(t, s.DB.Db, "fixtures/blob-sweep-001.sql")
// Incremental change tracking
tr, tr0 := easypg.NewTracker(t, s.DB.Db)
tr0.AssertEqualToFile("fixtures/setup.sql")
// ... run operation ...
tr.DBChanges().AssertEqual(`UPDATE repos SET next_sync_at = 7200 WHERE id = 1;`)
tr.DBChanges().AssertEmpty() // nothing else changedgo
// 在shared_test.go中——所有含数据库测试的包都必须实现
func TestMain(m *testing.M) {
easypg.WithTestDB(m, func() int { return m.Run() })
}
// 完整数据库快照断言
easypg.AssertDBContent(t, s.DB.Db, "fixtures/blob-sweep-001.sql")
// 增量变更跟踪
tr, tr0 := easypg.NewTracker(t, s.DB.Db)
tr0.AssertEqualToFile("fixtures/setup.sql")
// ... 执行操作 ...
tr.DBChanges().AssertEqual(`UPDATE repos SET next_sync_at = 7200 WHERE id = 1;`)
tr.DBChanges().AssertEmpty() // 无其他变更Test Execution Flags
测试执行标志
bash
go test -shuffle=on -p 1 -covermode=count -coverpkg=... -mod vendor ./...- : Randomize test order to detect order-dependent tests
-shuffle=on - : Sequential packages (shared PostgreSQL database)
-p 1 - : Use vendored dependencies
-mod vendor
bash
go test -shuffle=on -p 1 -covermode=count -coverpkg=... -mod vendor ./...- : 随机化测试顺序以检测依赖顺序的测试
-shuffle=on - : 按顺序测试包(共享PostgreSQL数据库)
-p 1 - : 使用 vendored 依赖
-mod vendor
Test Anti-Patterns
测试反模式
| Anti-Pattern | Correct Pattern |
|---|---|
| |
| Hand-written test doubles implementing real interfaces |
| |
| Inject |
| Log test case index: |
| 反模式 | 正确模式 |
|---|---|
| |
| 手写实现真实接口的测试替身 |
直接使用 | |
可测试代码中使用 | 注入 |
| 记录测试用例索引: |
8. Library Usage Rules
8. 库使用规则
APPROVED Libraries
已批准的库
| Library | Purpose | Key Pattern |
|---|---|---|
| Core framework (170+ files) | |
| | |
| Swift storage client | OpenStack storage driver only |
| HTTP routing | |
| CLI framework | |
| SQL ORM | |
| OpenStack SDK | Keystone auth, Swift storage |
| Metrics | Application + HTTP middleware metrics |
| Redis client | Rate limiting, token caching |
| UUID generation | NOT google/uuid, NOT satori/uuid |
| JWT tokens | Auth token handling |
| Testing only | In-memory Redis for tests |
| 库 | 用途 | 关键模式 |
|---|---|---|
| 核心框架(170+文件) | |
| | |
| Swift存储客户端 | 仅用于OpenStack存储驱动 |
| HTTP路由 | |
| CLI框架 | |
| SQL ORM | |
| OpenStack SDK | Keystone认证、Swift存储 |
| 指标 | 应用+HTTP中间件指标 |
| Redis客户端 | 速率限制、令牌缓存 |
| UUID生成 | 不要使用google/uuid或satori/uuid |
| JWT令牌 | 认证令牌处理 |
| 仅用于测试 | 内存Redis,用于测试 |
Related Libraries
相关库
| Library | Purpose | Key Pattern |
|---|---|---|
| | |
| Swift storage client | OpenStack storage driver only |
| 库 | 用途 | 关键模式 |
|---|---|---|
| | |
| Swift存储客户端 | 仅用于OpenStack存储驱动 |
FORBIDDEN Libraries
禁止使用的库
| Library | Reason | Use Instead |
|---|---|---|
| SAP CC has own testing framework | |
| SAP CC standardized on simple logging | |
| SAP CC uses stdlib + gorilla/mux | |
| Lightweight ORM preference | |
| No config files; env-var-only config | |
| Different UUID library chosen | |
| Manual test double implementations | Hand-written doubles via driver interfaces |
| Deprecated since Go 1.16 | |
| Global mutable state | |
| Archived, has CVEs | |
See references/library-reference.md for the complete table with versions and usage counts.
| 库 | 原因 | 替代方案 |
|---|---|---|
| SAP CC有自己的测试框架 | |
| SAP CC已标准化使用简单日志 | |
| SAP CC使用标准库+gorilla/mux | |
| 偏好轻量级ORM | |
| 不使用配置文件;仅使用环境变量配置 | |
| 已选择其他UUID库 | |
| 手动实现测试替身 | 通过驱动接口手写替身 |
| 自Go 1.16起已废弃 | 使用 |
| 全局可变状态 | |
| 已归档,存在CVE | |
查看references/library-reference.md获取包含版本和使用次数的完整表格。
Import Grouping Convention
导入分组约定
Three groups, separated by blank lines. Enforced by :
goimports -local github.com/sapcc/keppelgo
import (
// Group 1: Standard library
"context"
"encoding/json"
"fmt"
"net/http"
// Group 2: External (includes sapcc/go-bits, NOT local project)
"github.com/gorilla/mux"
. "github.com/majewsky/gg/option" // ONLY dot-import allowed
"github.com/sapcc/go-bits/httpapi"
"github.com/sapcc/go-bits/logg"
// Group 3: Local project
"github.com/sapcc/keppel/internal/keppel"
"github.com/sapcc/keppel/internal/models"
)Dot-import whitelist (only these 3 packages):
github.com/majewsky/gg/optiongithub.com/onsi/ginkgo/v2github.com/onsi/gomega
分为三组,用空行分隔。通过强制执行:
goimports -local github.com/sapcc/keppelgo
import (
// 第一组:标准库
"context"
"encoding/json"
"fmt"
"net/http"
// 第二组:外部库(包括sapcc/go-bits,不包含本地项目)
"github.com/gorilla/mux"
. "github.com/majewsky/gg/option" // 仅允许对此包使用点导入
"github.com/sapcc/go-bits/httpapi"
"github.com/sapcc/go-bits/logg"
// 第三组:本地项目
"github.com/sapcc/keppel/internal/keppel"
"github.com/sapcc/keppel/internal/models"
)点导入白名单(仅这3个包):
github.com/majewsky/gg/optiongithub.com/onsi/ginkgo/v2github.com/onsi/gomega
9. Build and CI Rules
9. 构建与CI规则
go-makefile-maker
go-makefile-maker
All build config is generated from . Do NOT edit these files directly:
Makefile.maker.yamlMakefile.golangci.yamlREUSE.toml.typos.toml- GitHub Actions workflows
所有构建配置从生成。请勿直接编辑以下文件:
Makefile.maker.yamlMakefile.golangci.yamlREUSE.toml.typos.toml- GitHub Actions工作流
License Headers (REQUIRED on every .go file)
许可证头(每个.go文件都必须包含)
go
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0go
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0golangci-lint v2 Configuration
golangci-lint v2配置
35+ enabled linters. Key settings:
| Setting | Value | Rationale |
|---|---|---|
| | |
| | Only flag strings repeated 5+ times |
| | Blank line after multi-line function signatures |
| | |
| all enabled | |
| disabled | Not worth the churn |
errcheck excluded functions (return values may be ignored):
encoding/json.Marshal(net/http.ResponseWriter).Write(*github.com/spf13/cobra.Command).Help
启用了35+个检查器。关键设置:
| 设置 | 值 | 理由 |
|---|---|---|
| | 标记 |
| | 仅标记重复5次以上的字符串 |
| | 多行函数签名后需要空行 |
| | |
| 全部启用 | 使用 |
| 禁用 | 不值得为此修改代码 |
errcheck排除的函数(返回值可忽略):
encoding/json.Marshal(net/http.ResponseWriter).Write(*github.com/spf13/cobra.Command).Help
revive Linter (REQUIRED — check on every sapcc repo)
revive检查器(必须使用——检查所有sapcc仓库)
As of 2026-03-20, go-makefile-maker supports as an optional linter. All sapcc repos should enable it. When working in an sapcc repo, check for . If absent, recommend adding:
reviveMakefile.maker.yamlreviveRulesyaml
golangciLint:
reviveRules:
- name: exported
arguments:
- checkPrivateReceivers
- disableChecksOnConstantsThis catches:
- Exported functions/types/methods without doc comments
- Private receivers on exported methods
After adding, run to regenerate , then to verify.
go-makefile-maker.golangci.yamlmake run-golangci-lint截至2026-03-20,go-makefile-maker支持作为可选检查器。所有sapcc仓库都应启用它。 在sapcc仓库中工作时,检查是否有。如果没有,建议添加:
reviveMakefile.maker.yamlreviveRulesyaml
golangciLint:
reviveRules:
- name: exported
arguments:
- checkPrivateReceivers
- disableChecksOnConstants这会捕获:
- 导出的函数/类型/方法没有文档注释
- 导出方法使用私有接收器
添加后,运行重新生成,然后运行验证。
go-makefile-maker.golangci.yamlmake run-golangci-lintBuild Commands
构建命令
bash
make build-all # Build binary
make check # Static checks + tests + build
make static-check # Lint + shellcheck + license checks
make run-golangci-lint # Lint only
make goimports # Format imports
make vendor # go mod tidy + vendor + verifybash
make build-all # 构建二进制文件
make check # 静态检查 + 测试 + 构建
make static-check # 代码检查 + shellcheck + 许可证检查
make run-golangci-lint # 仅代码检查
make goimports # 格式化导入
make vendor # go mod tidy + vendor + verify10. go-bits Design Philosophy
10. go-bits设计哲学
The go-bits library design rules that govern all of . Understanding these rules helps predict what code will pass review.
sapcc/go-bits管理所有代码的go-bits库设计规则。理解这些规则有助于预测哪些代码能通过评审。
sapcc/go-bitsRule 1: One Package = One Concept
规则1:一个包 = 一个概念
mustloggrespondwithmustloggrespondwithRule 2: Minimal API Surface
规则2:最小API表面
mustloggsyncextmustloggsyncextRule 3: Names That Read as English
规则3:名称读起来像英文
go
must.Succeed(err) // "must succeed"
must.Return(os.ReadFile(f)) // "must return"
respondwith.JSON(w, 200, d) // "respond with JSON"
logg.Fatal(msg) // "log fatal"
errext.As[T](err) // "error extension: as T"go
must.Succeed(err) // "必须成功"
must.Return(os.ReadFile(f)) // "必须返回"
respondwith.JSON(w, 200, d) // "以JSON响应"
logg.Fatal(msg) // "记录致命错误"
errext.As[T](err) // "错误扩展:转换为T"Rule 4: Document the WHY, Not Just the WHAT
规则4:记录原因,而不仅仅是内容
Extensive comments explaining design constraints and rejected alternatives. has three paragraphs explaining why the signature is the only one that works given Go generics limitations.
must.ReturnT大量注释解释设计约束和被拒绝的替代方案。有三段注释解释为什么在Go泛型的限制下,这个签名是唯一可行的。
must.ReturnTRule 5: Panics for Programming Errors, Errors for Runtime Failures
规则5:Panic用于编程错误,Error用于运行时失败
- Panic: nil factory in , calling API outside
pluggable.Add, mixing incompatible optionsCompose - Error return: missing env var, failed SQL query, JSON marshal failure
- Fatal (os.Exit): for genuinely unrecoverable startup errors
must.Succeed
- Panic:中的nil工厂、在
pluggable.Add之外调用API、混合不兼容选项Compose - 返回Error:缺少环境变量、SQL查询失败、JSON序列化失败
- Fatal(os.Exit):用于真正无法恢复的启动错误
must.Succeed
Rule 6: Concrete Before/After Examples in Docs
规则6:文档中包含具体的前后示例
Every function's godoc shows the exact code it replaces.
每个函数的godoc都展示了它所替代的精确代码。
Rule 7: Enforce Correct Usage Through Type System
规则7:通过类型系统强制正确使用
jobloop.Setup()jobloop.Setup()Rule 8: Dependency Consciousness
规则8:关注依赖
Actively prevents unnecessary dependency trees. Importing UUID from into was rejected because it would pull in AMQP dependencies. Solution: move to internal package.
audittoolsrespondwith主动避免不必要的依赖树。将UUID从导入到的提议被拒绝,因为这会引入AMQP依赖。解决方案:移到内部包。
audittoolsrespondwithRule 9: Prefer Functions Over Global Variables
规则9:优先使用函数而非全局变量
"I don't like having a global variable for this that callers can mess with."
Use instead of .
ForeachOptionTypeInLIQUID[T any](action func(any) T) []Tvar LiquidOptionTypes = []any{...}"我不喜欢用全局变量来实现这个,因为调用者可能会修改它。"
使用而非。
ForeachOptionTypeInLIQUID[T any](action func(any) T) []Tvar LiquidOptionTypes = []any{...}Rule 10: Leverage Go Generics Judiciously
规则10:明智地利用Go泛型
Use generics where they eliminate boilerplate or improve type safety:
- preserves return type
must.Return[V] - eliminates pointer-to-pointer pattern
errext.As[T] - constrains plugin types
pluggable.Registry[T Plugin]
Do NOT use generics where they add complexity without clear benefit.
在以下场景使用泛型:消除样板代码或提高类型安全性:
- 保留返回类型
must.Return[V] - 消除指针到指针的模式
errext.As[T] - 约束插件类型
pluggable.Registry[T Plugin]
不要在泛型会增加复杂度而无明显益处的场景使用它。
Rule 11: Graceful Deprecation
规则11:优雅地弃用
assert.HTTPRequestassert.HTTPRequestRule 12: Defense in Depth with Documentation
规则12:通过文档实现纵深防御
Handle theoretically impossible cases with branches that behave the same, and document the invariant reasoning.
处理理论上不可能的情况,分支行为保持一致,并记录不变量推理。
Error Handling
错误处理
Error: "Cannot find go-bits dependency"
错误:"Cannot find go-bits dependency"
Cause: Project does not import
Solution: This skill only applies to sapcc projects. Check first.
github.com/sapcc/go-bitsgo.mod原因:项目未导入
解决方案:本规范仅适用于sapcc项目。请先检查。
github.com/sapcc/go-bitsgo.modError: "Linter reports forbidden import"
错误:"Linter reports forbidden import"
Cause: Using a FORBIDDEN library (testify, zap, gin, etc.)
Solution: Replace with the SAP CC equivalent. See the FORBIDDEN table in Section 8.
原因:使用了禁止的库(testify、zap、gin等)
解决方案:替换为SAP CC的等效库。请查看第8节的禁止库表格。
Error: "Missing SPDX license header"
错误:"Missing SPDX license header"
Cause: file missing the required two-line SPDX header
Solution: Add and as the first two lines.
.go// SPDX-FileCopyrightText: <year> SAP SE or an SAP affiliate company// SPDX-License-Identifier: Apache-2.0原因:.go文件缺少必需的两行SPDX许可证头
解决方案:在文件开头添加和。
// SPDX-FileCopyrightText: <年份> SAP SE or an SAP affiliate company// SPDX-License-Identifier: Apache-2.0Error: "Import groups out of order"
错误:"Import groups out of order"
Cause: Imports not in the three-group order (stdlib / external / local)
Solution: Run .
goimports -w -local github.com/sapcc/keppel <file>原因:导入未按三组顺序排列(标准库 / 外部库 / 本地项目)
解决方案:运行。
goimports -w -local github.com/sapcc/keppel <文件>Error: "Test uses testify/assert"
错误:"Test uses testify/assert"
Cause: Mixing assertion libraries
Solution: Replace (testify) with (go-bits). Note the parameter order difference.
assert.Equal(t, expected, actual)assert.DeepEqual(t, "desc", actual, expected)原因:混合使用了断言库
解决方案:将testify的替换为go-bits的。注意参数顺序的差异。
assert.Equal(t, expected, actual)assert.DeepEqual(t, "desc", actual, expected)Anti-Patterns
反模式
See references/anti-patterns.md for the full catalog with BAD/GOOD examples.
查看references/anti-patterns.md获取包含错误/正确示例的完整目录。
AP-1: Creating Types for One-Off JSON Marshaling
AP-1:为一次性JSON序列化创建类型
What it looks like: Struct types with json tags used once for
Why wrong: This is considered "overengineered" by project convention
Do instead: with
json.Marshalfmt.Sprintfmust.Return(json.Marshal(dynamicPart))表现:创建带json标签的结构体类型,仅用于一次
错误原因:项目约定认为这是“过度工程”
正确做法:使用 +
json.Marshalfmt.Sprintfmust.Return(json.Marshal(dynamicPart))AP-2: Wrapping Errors That Already Have Context
AP-2:包装已包含上下文的错误
What it looks like:
Why wrong: strconv already includes function name, input, and error type
Do instead:
fmt.Errorf("parse error: %w", strconv.ParseUint(...))must.Return(strconv.ParseUint(s, 10, 32))表现:
错误原因:strconv已包含函数名、输入和错误类型
正确做法:
fmt.Errorf("parse error: %w", strconv.ParseUint(...))must.Return(strconv.ParseUint(s, 10, 32))AP-3: Manual Argument Dispatch Instead of Cobra
AP-3:手动参数分发而非使用Cobra
What it looks like: Switch statement on to dispatch to code paths
Why wrong: Cobra subcommands handle this with better UX
Do instead: Change argument order if needed to allow Cobra subcommands
args[0]表现:使用switch语句基于分发到不同代码路径
错误原因:Cobra子命令能以更好的用户体验处理此问题
正确做法:如有需要,修改参数顺序以支持Cobra子命令
args[0]AP-4: Using must.Return in Request Handlers
AP-4:在请求处理器中使用must.Return
What it looks like: inside an HTTP handler
Why wrong: calls on error, crashing the server
Do instead: Return errors properly; is for startup code and tests only
val := must.Return(someOperation())must.Returnos.Exit(1)must.*表现:在HTTP处理器中使用
错误原因:在错误时会调用,导致服务器崩溃
正确做法:正确返回错误;仅用于启动代码和测试
val := must.Return(someOperation())must.Returnos.Exit(1)must.*AP-5: Global Mutable Variables for Configuration
AP-5:使用全局可变变量存储配置
What it looks like: at package level
Why wrong: Callers can modify the map, creating inconsistent state
Do instead: Functions that produce values:
var Config = map[string]string{...}func GetConfig() map[string]string表现:包级别变量
错误原因:调用者可以修改该映射,导致状态不一致
正确做法:使用生成值的函数:
var Config = map[string]string{...}func GetConfig() map[string]stringAvailable Scripts
可用脚本
Deterministic checks for sapcc-specific patterns that no linter covers. Run these during code review or as part of quality gates. All support , , , and meaningful exit codes (0 = clean, 1 = violations, 2 = error).
--help--json--limit| Script | What It Checks |
|---|---|
| HTTP handlers missing |
| Data access before authentication in handlers |
| |
| Direct |
| |
| Bare TODO comments without context/links |
These scripts only apply to sapcc repos (detected by in go.mod).
github.com/sapcc/go-bits用于检测特定sapcc模式的确定性检查,这些模式没有检查器覆盖。在代码评审期间或作为质量门运行这些脚本。所有脚本都支持、、和有意义的退出码(0 = 无问题,1 = 违反规则,2 = 错误)。
--help--json--limit| 脚本 | 检查内容 |
|---|---|
| HTTP处理器缺少 |
| 处理器中数据访问在认证之前 |
| |
| 可测试代码中直接使用 |
| 使用 |
| 无上下文/链接的裸TODO注释 |
这些脚本仅适用于sapcc仓库(通过go.mod中的检测)。
github.com/sapcc/go-bitsAnti-Rationalization
反合理化
SAP CC Domain-Specific Rationalizations
SAP CC领域特定的合理化反驳
| Rationalization | Why It's Wrong | Required Action |
|---|---|---|
| "Tests pass, the error wrapping is fine" | Lead review checks error message quality in review | Verify error context matches project standards |
| "Copilot suggested this approach" | Lead review frequently rejects Copilot suggestions as overengineered | Evaluate on merit, simplify where possible |
| "I need a struct for this JSON" | One-off JSON can be | Only create types if reused or complex |
| "Better safe than sorry" (re: error handling) | "Irrelevant contrivance" -- handle only practical concerns | Ask "concrete scenario where this fails?" |
| "Standard library X works fine here" | SAP CC has go-bits equivalents that are expected | Use go-bits (logg, assert, must, osext, respondwith) |
| "testify is the Go standard" | SAP CC projects use go-bits/assert exclusively | Never introduce testify in sapcc repos |
| "I'll add comprehensive error wrapping" | Trust well-designed functions' error messages | Check if the called function already provides context |
| "This needs a config file" | SAP CC uses env vars only | Use osext.MustGetenv, GetenvOrDefault, GetenvBool |
| 合理化理由 | 错误原因 | 要求操作 |
|---|---|---|
| "测试通过,错误包装没问题" | 主评审会在评审中检查错误消息质量 | 验证错误上下文是否符合项目标准 |
| "Copilot建议了这种方法" | 主评审经常拒绝Copilot的过度工程建议 | 根据实际价值评估,尽可能简化 |
| "我需要一个结构体来处理这个JSON" | 一次性JSON可以使用 | 仅在复用或复杂时创建类型 |
| "谨慎总比抱歉好"(关于错误处理) | 被视为“无关紧要的矫揉造作”——仅处理实际问题 | 问自己“这个方案会失败的具体场景是什么?” |
| "标准库X在这里工作得很好" | SAP CC期望使用go-bits的等效功能 | 使用go-bits(logg、assert、must、osext、respondwith) |
| "testify是Go的标准" | SAP CC项目仅使用go-bits/assert | 永远不要在sapcc仓库中引入testify |
| "我需要添加全面的错误包装" | 信任设计良好的函数的错误消息 | 检查被调用函数是否已提供上下文 |
| "这需要一个配置文件" | SAP CC仅使用环境变量 | 使用osext.MustGetenv、GetenvOrDefault、GetenvBool |
References (MUST READ)
参考资料(必须阅读)
NON-NEGOTIABLE: Before working on ANY sapcc Go code, you MUST read these reference files. Do NOT skip them. Do NOT rely on your training data for sapcc conventions — read the actual references. These contain the real rules from actual PR reviews.
Load order (read in this sequence):
- sapcc-code-patterns.md — Read FIRST. This is the definitive reference with actual code patterns
- library-reference.md — Read SECOND. Know which libraries are approved/forbidden before writing imports
- architecture-patterns.md — Read THIRD if working on architecture, HTTP handlers, or DB access
- Then load others as needed for the specific task
| File | What It Contains | When to Read |
|---|---|---|
| references/sapcc-code-patterns.md | Actual code patterns — function signatures, constructors, interfaces, HTTP handlers, error handling, DB access, testing, package organization | ALWAYS — this is the primary reference |
| references/library-reference.md | Complete library table: 30 approved, 10+ forbidden, with versions and usage counts | ALWAYS — need to know approved/forbidden imports |
| references/architecture-patterns.md | Full 102-rule architecture specification with code examples | When working on architecture, handlers, DB access |
| references/review-standards-lead.md | All 21 lead review comments with full context and quotes | For reviews and understanding lead review reasoning |
| references/review-standards-secondary.md | All 15 secondary review comments with PR context | For reviews and understanding secondary review patterns |
| references/anti-patterns.md | 20+ SAP CC anti-patterns with BAD/GOOD code examples | For code review and avoiding common mistakes |
| references/extended-patterns.md | Extended patterns from related repos — security micro-patterns, visual section separators, copyright format, K8s namespace isolation, PR hygiene (sort lists, clean orphans, document alongside), changelog format. Pipeline-generated. | For security-conscious code, K8s helm work, or PR hygiene |
非协商要求:在处理任何sapcc Go代码之前,您必须阅读这些参考文件。不要跳过它们。不要依赖您的训练数据来了解sapcc约定——请阅读实际的参考资料。这些文件包含来自真实PR评审的真实规则。
阅读顺序(按此顺序阅读):
- sapcc-code-patterns.md —— 首先阅读。这是包含实际代码模式的权威参考
- library-reference.md —— 其次阅读。在编写导入之前,了解哪些库是批准/禁止的
- architecture-patterns.md —— 如果处理架构、HTTP处理器或数据库访问,第三阅读
- 然后根据具体任务需要加载其他文件
| 文件 | 内容 | 阅读时机 |
|---|---|---|
| references/sapcc-code-patterns.md | 实际代码模式 —— 函数签名、构造函数、接口、HTTP处理器、错误处理、数据库访问、测试、包组织 | 始终 —— 这是主要参考资料 |
| references/library-reference.md | 完整的库表格:30个已批准,10+个禁止,包含版本和使用次数 | 始终 —— 需要了解批准/禁止的导入 |
| references/architecture-patterns.md | 完整的102条架构规范,包含代码示例 | 处理架构、处理器、数据库访问时 |
| references/review-standards-lead.md | 所有21条主评审评论,包含完整上下文和引用 | 进行评审或理解主评审推理时 |
| references/review-standards-secondary.md | 所有15条次级评审评论,包含PR上下文 | 进行评审或理解次级评审模式时 |
| references/anti-patterns.md | 20+个SAP CC反模式,包含错误/正确代码示例 | 进行代码评审或避免常见错误时 |
| references/extended-patterns.md | 来自相关仓库的扩展模式 —— 安全微模式、可视化章节分隔符、版权格式、K8s命名空间隔离、PR卫生(排序列表、清理孤立代码、随代码文档化)、变更日志格式。由流水线生成。 | 处理安全相关代码、K8s helm工作或PR卫生时 |