go-sapcc-conventions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SAP Converged Cloud Go Conventions

SAP Converged Cloud Go编码规范

Coding standards extracted from extensive PR review analysis across
sapcc/keppel
and
sapcc/go-bits
. These are the real rules enforced in code review by the project's lead review standards.
本编码标准源自对
sapcc/keppel
sapcc/go-bits
大量PR评审的分析,是项目主评审实际强制执行的规则。

Tool Integration

工具集成

gopls MCP (MUST use when available): Use
go_workspace
at session start,
go_file_context
after reading .go files,
go_symbol_references
before modifying any symbol (critical for sapcc — lead review checks cross-package impact),
go_diagnostics
after every edit,
go_vulncheck
after go.mod changes. This gives type-aware analysis that catches issues grep cannot.
Modern Go Guidelines: Detect Go version from go.mod. Sapcc projects typically target Go 1.22+. Use version-appropriate features:
t.Context()
(1.24+),
b.Loop()
(1.24+),
strings.SplitSeq
(1.24+),
wg.Go()
(1.25+),
errors.AsType[T]
(1.26+).

gopls MCP(可用时必须使用):会话启动时使用
go_workspace
,读取.go文件后使用
go_file_context
,修改任何符号前使用
go_symbol_references
(这对sapcc至关重要——主评审会检查跨包影响),每次编辑后使用
go_diagnostics
,修改go.mod后使用
go_vulncheck
。这能提供类型感知分析,捕获grep无法发现的问题。
现代Go指南:从go.mod中检测Go版本。Sapcc项目通常以Go 1.22+为目标版本,使用对应版本的特性:
t.Context()
(1.24+)、
b.Loop()
(1.24+)、
strings.SplitSeq
(1.24+)、
wg.Go()
(1.25+)、
errors.AsType[T]
(1.26+)。

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
strconv
functions include the function name, input value, and error reason:
go
// 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标准库的
strconv
函数会包含函数名、输入值和错误原因:
go
// 错误示例:冗余包装
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
os.Stdout.Write()
, but not about the much more numerous instances of
fmt.Println
that theoretically suffer the same problem."
不要处理实际中永远不会触发的错误。请始终如一地遵循此标准:
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
defer Close()
on
io.NopCloser
just for theoretical contract compliance:
"This is an irrelevant contrivance. Either
WriteTrivyReport
does it, or the operation fails and we fatal-error out, in which case it does not matter anyway."
不要为
io.NopCloser
添加
defer 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
当已知未来会有设计变更时,不要构建会被打破的抽象:
undefined

BAD: 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

核心原则

RuleSummary
Trust the stdlibDon't wrap errors that
strconv
, constructors, etc. already describe well
Use Cobra subcommandsNever manually roll argument dispatch that Cobra handles
CLI names: specific + extensible
keppel test-driver storage
, not
keppel test
Marshal structured data for errorsIf you have a
map[string]any
,
json.Marshal
it instead of manually formatting fields
Tests must verify behaviorNever silently remove test assertions during refactoring
Explain test workaroundsAdd comments when test setup diverges from production patterns
Use existing error utilitiesUse
errext.ErrorSet
and
.Join()
, not manual string concatenation
TODOs need contextInclude what, a starting point link, and why not done now
Documentation stays qualifiedWhen behavior changes conditionally, update docs to state the conditions
Understand value semanticsValue receiver copies the struct, but reference-type fields share data
Variable names don't misleadDon't name script vars as if the application reads them
规则摘要
信任标准库不要包装
strconv
、构造函数等已提供良好描述的错误
使用Cobra子命令永远不要手动实现Cobra已能处理的参数分发
CLI命名:具体且可扩展使用
keppel test-driver storage
,而非
keppel test
为错误序列化结构化数据如果有
map[string]any
,使用
json.Marshal
而非手动格式化字段
测试必须验证行为重构时永远不要静默移除测试断言
解释测试变通方案当测试设置与生产模式不同时,添加注释说明
使用现有错误工具使用
errext.ErrorSet
.Join()
,而非手动字符串拼接
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

核心原则

RuleSummary
Error messages must be actionable"Internal Server Error" is unacceptable when the cause is knowable
Know the spec, deviate pragmaticallyReference RFCs, but deviate when spec is impractical
Guard against panics with clear errorsCheck nil/empty before indexing, use
fmt.Errorf("invalid: %q", val)
Strict configuration parsingUse
DisallowUnknownFields()
on JSON decoders for config
Test ALL combinationsWhen changing logic with multiple inputs, test every meaningful combination
Eliminate redundant codeAsk "This check is now redundant?" when code is refactored
Comments explain WHYWhen something non-obvious is added, request an explanatory comment
Domain knowledge over theoryDismiss concerns that don't apply to actual domain constraints
Smallest possible fix2-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 mistakesAcknowledge 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/空值,使用
fmt.Errorf("invalid: %q", val)
严格的配置解析对JSON解码器使用
DisallowUnknownFields()
测试所有组合当修改有多个输入的逻辑时,测试所有有意义的组合
消除冗余代码重构时问自己“这个检查现在是不是冗余了?”
注释解释原因当添加非显而易见的内容时,需要添加解释性注释
领域知识优先于理论拒绝不适用于实际领域约束的问题
尽可能最小的修复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, helpers
project/
  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
%q
, descriptive action prefix.
go
// "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)
所有错误消息:小写,无结尾标点,使用
%q
包含标识数据,带描述性动作前缀。

must.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 propagated
go
// 允许:启动/引导代码(致命错误)
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,
must
and
assert
serve different roles:
PackageCallsUse When
assert
t.Errorf
(non-fatal)
Checking the expected outcome of the operation being tested
must
t.Fatal
(fatal)
Setup/preconditions where failure means subsequent lines are meaningless
Decision tree:
  1. Inside a
    mustXxx
    helper? ->
    must.SucceedT
    /
    must.ReturnT
  2. Next line depends on this succeeding? ->
    must.SucceedT
    /
    must.ReturnT
  3. Checking the outcome of the tested operation? ->
    assert.ErrEqual(t, err, nil)
  4. Need a return value? ->
    must.ReturnT
    (no assert equivalent)
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.
在测试代码中,
must
assert
有不同的作用:
调用方式使用场景
assert
t.Errorf
(非致命)
检查被测试操作的预期结果
must
t.Fatal
(致命)
设置/前置条件,失败意味着后续代码无意义
决策树:
  1. mustXxx
    助手函数中? -> 使用
    must.SucceedT
    /
    must.ReturnT
  2. 下一行代码依赖此操作成功? -> 使用
    must.SucceedT
    /
    must.ReturnT
  3. 检查被测试操作的结果? -> 使用
    assert.ErrEqual(t, err, nil)
  4. 需要返回值? -> 使用
    must.ReturnT
    (assert无等价方法)
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
==
?
UseArgs
Yes (int, string, bool)
assert.Equal(t, actual, expected)
3
No (slices, maps, structs)
assert.DeepEqual(t, "label", actual, expected)
4
Common mistake flagged in review:
assert.DeepEqual(t, "count", len(events), 3)
len
returns
int
which is comparable, so use
assert.Equal(t, len(events), 3)
.
类型支持
==
使用方法参数
是(int、string、bool)
assert.Equal(t, actual, expected)
3个
否(切片、映射、结构体)
assert.DeepEqual(t, "label", actual, expected)
4个
评审中常见的错误:
assert.DeepEqual(t, "count", len(events), 3)
——
len
返回的
int
是可比较的,应使用
assert.Equal(t, len(events), 3)

Logging Level Selection

日志级别选择

LevelWhenExample
logg.Fatal
Startup/CLI only, never in handlers
logg.Fatal("failed to read key: %s", err.Error())
logg.Error
Cannot bubble up (cleanup, deferred, advisory)
logg.Error("rollback failed: " + err.Error())
logg.Info
Operational events, graceful degradation
logg.Info("rejecting overlong name: %q", name)
logg.Debug
Diagnostic, gated behind
KEPPEL_DEBUG
logg.Debug("parsing configuration...")
级别使用场景示例
logg.Fatal
仅用于启动/CLI,永远不要在处理器中使用
logg.Fatal("failed to read key: %s", err.Error())
logg.Error
无法向上传播的错误(清理、延迟操作、通知)
logg.Error("rollback failed: " + err.Error())
logg.Info
操作事件、优雅降级
logg.Info("rejecting overlong name: %q", name)
logg.Debug
诊断信息,受
KEPPEL_DEBUG
控制
logg.Debug("parsing configuration...")

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.Read
    ,
    json.Marshal
    on known-good data
  • 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.Read
    、对已知有效数据的
    json.Marshal
  • 初始化顺序违反:
    panic("called before Connect()")
永远不要在以下场景使用Panic:用户输入、外部服务、数据库错误、请求处理。

HTTP Error Response Formats (3 distinct)

HTTP错误响应格式(3种不同格式)

API SurfaceFormatHelper
Registry V2 (
/v2/
)
JSON
{"errors": [{code, message, detail}]}
rerr.WriteAsRegistryV2ResponseTo(w, r)
Keppel V1 (
/keppel/v1/
)
Obfuscated text (5xx get UUID-masked)
respondwith.ObfuscatedErrorText(w, err)
Auth (
/keppel/v1/auth
)
JSON
{"details": "..."}
rerr.WriteAsAuthResponseTo(w)
5xx errors use
respondwith.ObfuscatedErrorText
which logs the real error with a UUID and returns
"Internal Server Error (ID = <uuid>)"
to the client.

API表面格式助手函数
Registry V2 (
/v2/
)
JSON
{"errors": [{code, message, detail}]}
rerr.WriteAsRegistryV2ResponseTo(w, r)
Keppel V1 (
/keppel/v1/
)
模糊文本(5xx错误会被UUID掩码)
respondwith.ObfuscatedErrorText(w, err)
Auth (
/keppel/v1/auth
)
JSON
{"details": "..."}
rerr.WriteAsAuthResponseTo(w)
5xx错误使用
respondwith.ObfuscatedErrorText
,它会用UUID记录真实错误,并向客户端返回
"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:
    go-bits/assert
    (NOT testify, NOT gomock)
  • DB testing:
    easypg.WithTestDB
    in every
    TestMain
  • Test setup: Functional options via
    test.NewSetup(t, ...options)
  • HTTP testing:
    assert.HTTPRequest{}.Check(t, handler)
  • Time control:
    mock.Clock
    (never call
    time.Now()
    directly)
  • Test doubles: Implement real driver interfaces, register via
    init()
  • 断言库
    go-bits/assert
    不要使用testify,不要使用gomock)
  • 数据库测试:每个
    TestMain
    中使用
    easypg.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 changed
go
// 在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 ./...
  • -shuffle=on
    : Randomize test order to detect order-dependent tests
  • -p 1
    : Sequential packages (shared PostgreSQL database)
  • -mod vendor
    : Use vendored dependencies
bash
go test -shuffle=on -p 1 -covermode=count -coverpkg=... -mod vendor ./...
  • -shuffle=on
    : 随机化测试顺序以检测依赖顺序的测试
  • -p 1
    : 按顺序测试包(共享PostgreSQL数据库)
  • -mod vendor
    : 使用 vendored 依赖

Test Anti-Patterns

测试反模式

Anti-PatternCorrect Pattern
testify/assert
go-bits/assert
gomock
/
mockery
Hand-written test doubles implementing real interfaces
httptest.NewRecorder
directly
assert.HTTPRequest{}.Check(t, h)
time.Now()
in testable code
Inject
func() time.Time
, use
mock.Clock
t.Run
subtests (rare in keppel)
Log test case index:
t.Logf("----- testcase %d/%d -----")

反模式正确模式
testify/assert
go-bits/assert
gomock
/
mockery
手写实现真实接口的测试替身
直接使用
httptest.NewRecorder
assert.HTTPRequest{}.Check(t, h)
可测试代码中使用
time.Now()
注入
func() time.Time
,使用
mock.Clock
t.Run
子测试(Keppel中很少使用)
记录测试用例索引:
t.Logf("----- testcase %d/%d -----")

8. Library Usage Rules

8. 库使用规则

APPROVED Libraries

已批准的库

LibraryPurposeKey Pattern
sapcc/go-bits
Core framework (170+ files)
logg.Info
,
must.Return
,
assert.HTTPRequest
majewsky/gg/option
Option[T]
(45 files)
Some(v)
,
None[T]()
, dot-import ONLY for this
majewsky/schwift/v2
Swift storage clientOpenStack storage driver only
gorilla/mux
HTTP routing
r.Methods("GET").Path("/path").HandlerFunc(h)
spf13/cobra
CLI framework
AddCommandTo(parent)
pattern
go-gorp/gorp/v3
SQL ORM
db:"column_name"
struct tags
gophercloud/gophercloud/v2
OpenStack SDKKeystone auth, Swift storage
prometheus/client_golang
MetricsApplication + HTTP middleware metrics
redis/go-redis/v9
Redis clientRate limiting, token caching
gofrs/uuid/v5
UUID generationNOT google/uuid, NOT satori/uuid
golang-jwt/jwt/v5
JWT tokensAuth token handling
alicebob/miniredis/v2
Testing onlyIn-memory Redis for tests
用途关键模式
sapcc/go-bits
核心框架(170+文件)
logg.Info
,
must.Return
,
assert.HTTPRequest
majewsky/gg/option
Option[T]
(45个文件)
Some(v)
,
None[T]()
, 仅对此包使用点导入
majewsky/schwift/v2
Swift存储客户端仅用于OpenStack存储驱动
gorilla/mux
HTTP路由
r.Methods("GET").Path("/path").HandlerFunc(h)
spf13/cobra
CLI框架
AddCommandTo(parent)
模式
go-gorp/gorp/v3
SQL ORM
db:"column_name"
结构体标签
gophercloud/gophercloud/v2
OpenStack SDKKeystone认证、Swift存储
prometheus/client_golang
指标应用+HTTP中间件指标
redis/go-redis/v9
Redis客户端速率限制、令牌缓存
gofrs/uuid/v5
UUID生成不要使用google/uuid或satori/uuid
golang-jwt/jwt/v5
JWT令牌认证令牌处理
alicebob/miniredis/v2
仅用于测试内存Redis,用于测试

Related Libraries

相关库

LibraryPurposeKey Pattern
majewsky/gg/option
Option[T]
(45 files)
Some(v)
,
None[T]()
, dot-import ONLY for this
majewsky/schwift/v2
Swift storage clientOpenStack storage driver only
用途关键模式
majewsky/gg/option
Option[T]
(45个文件)
Some(v)
,
None[T]()
, 仅对此包使用点导入
majewsky/schwift/v2
Swift存储客户端仅用于OpenStack存储驱动

FORBIDDEN Libraries

禁止使用的库

LibraryReasonUse Instead
testify
(assert/require/mock)
SAP CC has own testing framework
go-bits/assert
+
go-bits/must
zap
/
zerolog
/
slog
/
logrus
SAP CC standardized on simple logging
go-bits/logg
gin
/
echo
/
fiber
SAP CC uses stdlib + gorilla/mux
go-bits/httpapi
+
gorilla/mux
gorm
/
sqlx
/
ent
Lightweight ORM preference
go-gorp/gorp/v3
+
go-bits/sqlext
viper
No config files; env-var-only config
go-bits/osext
+
os.Getenv
google/uuid
/
satori/uuid
Different UUID library chosen
gofrs/uuid/v5
gomock
/
mockery
Manual test double implementationsHand-written doubles via driver interfaces
ioutil.*
Deprecated since Go 1.16
os
and
io
packages
http.DefaultServeMux
Global mutable state
http.NewServeMux()
gopkg.in/square/go-jose.v2
Archived, has CVEs
gopkg.in/go-jose/go-jose.v2
See references/library-reference.md for the complete table with versions and usage counts.
原因替代方案
testify
(assert/require/mock)
SAP CC有自己的测试框架
go-bits/assert
+
go-bits/must
zap
/
zerolog
/
slog
/
logrus
SAP CC已标准化使用简单日志
go-bits/logg
gin
/
echo
/
fiber
SAP CC使用标准库+gorilla/mux
go-bits/httpapi
+
gorilla/mux
gorm
/
sqlx
/
ent
偏好轻量级ORM
go-gorp/gorp/v3
+
go-bits/sqlext
viper
不使用配置文件;仅使用环境变量配置
go-bits/osext
+
os.Getenv
google/uuid
/
satori/uuid
已选择其他UUID库
gofrs/uuid/v5
gomock
/
mockery
手动实现测试替身通过驱动接口手写替身
ioutil.*
自Go 1.16起已废弃使用
os
io
http.DefaultServeMux
全局可变状态
http.NewServeMux()
gopkg.in/square/go-jose.v2
已归档,存在CVE
gopkg.in/go-jose/go-jose.v2
查看references/library-reference.md获取包含版本和使用次数的完整表格。

Import Grouping Convention

导入分组约定

Three groups, separated by blank lines. Enforced by
goimports -local github.com/sapcc/keppel
:
go
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/option
  • github.com/onsi/ginkgo/v2
  • github.com/onsi/gomega

分为三组,用空行分隔。通过
goimports -local github.com/sapcc/keppel
强制执行:
go
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/option
  • github.com/onsi/ginkgo/v2
  • github.com/onsi/gomega

9. Build and CI Rules

9. 构建与CI规则

go-makefile-maker

go-makefile-maker

All build config is generated from
Makefile.maker.yaml
. Do NOT edit these files directly:
  • Makefile
  • .golangci.yaml
  • REUSE.toml
  • .typos.toml
  • GitHub Actions workflows
所有构建配置从
Makefile.maker.yaml
生成。请勿直接编辑以下文件:
  • Makefile
  • .golangci.yaml
  • REUSE.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.0
go
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

golangci-lint v2 Configuration

golangci-lint v2配置

35+ enabled linters. Key settings:
SettingValueRationale
errcheck.check-blank
true
_ = err
is flagged
goconst.min-occurrences
5
Only flag strings repeated 5+ times
whitespace.multi-func
true
Blank line after multi-line function signatures
nolintlint.require-specific
true
//nolint
must specify which linter
usestdlibvars
all enabled
http.StatusOK
not
200
,
http.MethodGet
not
"GET"
govet.fieldalignment
disabledNot 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+个检查器。关键设置:
设置理由
errcheck.check-blank
true
标记
_ = err
goconst.min-occurrences
5
仅标记重复5次以上的字符串
whitespace.multi-func
true
多行函数签名后需要空行
nolintlint.require-specific
true
//nolint
必须指定检查器
usestdlibvars
全部启用使用
http.StatusOK
而非
200
,使用
http.MethodGet
而非
"GET"
govet.fieldalignment
禁用不值得为此修改代码
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
revive
as an optional linter. All sapcc repos should enable it. When working in an sapcc repo, check
Makefile.maker.yaml
for
reviveRules
. If absent, recommend adding:
yaml
golangciLint:
  reviveRules:
    - name: exported
      arguments:
        - checkPrivateReceivers
        - disableChecksOnConstants
This catches:
  • Exported functions/types/methods without doc comments
  • Private receivers on exported methods
After adding, run
go-makefile-maker
to regenerate
.golangci.yaml
, then
make run-golangci-lint
to verify.
截至2026-03-20,go-makefile-maker支持
revive
作为可选检查器。所有sapcc仓库都应启用它。 在sapcc仓库中工作时,检查
Makefile.maker.yaml
是否有
reviveRules
。如果没有,建议添加:
yaml
golangciLint:
  reviveRules:
    - name: exported
      arguments:
        - checkPrivateReceivers
        - disableChecksOnConstants
这会捕获:
  • 导出的函数/类型/方法没有文档注释
  • 导出方法使用私有接收器
添加后,运行
go-makefile-maker
重新生成
.golangci.yaml
,然后运行
make run-golangci-lint
验证。

Build 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 + verify

bash
make build-all              # 构建二进制文件
make check                  # 静态检查 + 测试 + 构建
make static-check           # 代码检查 + shellcheck + 许可证检查
make run-golangci-lint      # 仅代码检查
make goimports              # 格式化导入
make vendor                 # go mod tidy + vendor + verify

10. go-bits Design Philosophy

10. go-bits设计哲学

The go-bits library design rules that govern all of
sapcc/go-bits
. Understanding these rules helps predict what code will pass review.
管理所有
sapcc/go-bits
代码的go-bits库设计规则。理解这些规则有助于预测哪些代码能通过评审。

Rule 1: One Package = One Concept

规则1:一个包 = 一个概念

must
= fatal errors.
logg
= logging.
respondwith
= HTTP responses. No package does two things.
must
= 致命错误。
logg
= 日志。
respondwith
= HTTP响应。没有包会同时处理两件事。

Rule 2: Minimal API Surface

规则2:最小API表面

must
has 4 functions.
logg
has 5.
syncext
has 1 type with 3 methods. Fewer, more general functions beat many specific ones.
must
有4个函数。
logg
有5个。
syncext
有1个类型和3个方法。更少、更通用的函数优于许多特定函数。

Rule 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.
must.ReturnT
has three paragraphs explaining why the signature is the only one that works given Go generics limitations.
大量注释解释设计约束和被拒绝的替代方案。
must.ReturnT
有三段注释解释为什么在Go泛型的限制下,这个签名是唯一可行的。

Rule 5: Panics for Programming Errors, Errors for Runtime Failures

规则5:Panic用于编程错误,Error用于运行时失败

  • Panic: nil factory in
    pluggable.Add
    , calling API outside
    Compose
    , mixing incompatible options
  • Error return: missing env var, failed SQL query, JSON marshal failure
  • Fatal (os.Exit):
    must.Succeed
    for genuinely unrecoverable startup errors
  • Panic
    pluggable.Add
    中的nil工厂、在
    Compose
    之外调用API、混合不兼容选项
  • 返回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()
returns a private type wrapping the struct, enforcing that Setup was called.
jobloop.Setup()
返回一个包装结构体的私有类型,强制要求必须先调用Setup。

Rule 8: Dependency Consciousness

规则8:关注依赖

Actively prevents unnecessary dependency trees. Importing UUID from
audittools
into
respondwith
was rejected because it would pull in AMQP dependencies. Solution: move to internal package.
主动避免不必要的依赖树。将UUID从
audittools
导入到
respondwith
的提议被拒绝,因为这会引入AMQP依赖。解决方案:移到内部包。

Rule 9: Prefer Functions Over Global Variables

规则9:优先使用函数而非全局变量

"I don't like having a global variable for this that callers can mess with."
Use
ForeachOptionTypeInLIQUID[T any](action func(any) T) []T
instead of
var LiquidOptionTypes = []any{...}
.
"我不喜欢用全局变量来实现这个,因为调用者可能会修改它。"
使用
ForeachOptionTypeInLIQUID[T any](action func(any) T) []T
而非
var LiquidOptionTypes = []any{...}

Rule 10: Leverage Go Generics Judiciously

规则10:明智地利用Go泛型

Use generics where they eliminate boilerplate or improve type safety:
  • must.Return[V]
    preserves return type
  • errext.As[T]
    eliminates pointer-to-pointer pattern
  • pluggable.Registry[T Plugin]
    constrains plugin types
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.HTTPRequest
is deprecated but not removed. The deprecation notice includes a complete migration guide. No forced migration.
assert.HTTPRequest
已被弃用但未被移除。弃用通知包含完整的迁移指南。不强制迁移。

Rule 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
github.com/sapcc/go-bits
Solution: This skill only applies to sapcc projects. Check
go.mod
first.
原因:项目未导入
github.com/sapcc/go-bits
解决方案:本规范仅适用于sapcc项目。请先检查
go.mod

Error: "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:
.go
file missing the required two-line SPDX header Solution: Add
// SPDX-FileCopyrightText: <year> SAP SE or an SAP affiliate company
and
// SPDX-License-Identifier: Apache-2.0
as the first two lines.
原因:.go文件缺少必需的两行SPDX许可证头 解决方案:在文件开头添加
// SPDX-FileCopyrightText: <年份> SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

Error: "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
assert.Equal(t, expected, actual)
(testify) with
assert.DeepEqual(t, "desc", actual, expected)
(go-bits). Note the parameter order difference.

原因:混合使用了断言库 解决方案:将testify的
assert.Equal(t, expected, actual)
替换为go-bits的
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
json.Marshal
Why wrong: This is considered "overengineered" by project convention Do instead:
fmt.Sprintf
with
must.Return(json.Marshal(dynamicPart))
表现:创建带json标签的结构体类型,仅用于一次
json.Marshal
错误原因:项目约定认为这是“过度工程” 正确做法:使用
fmt.Sprintf
+
must.Return(json.Marshal(dynamicPart))

AP-2: Wrapping Errors That Already Have Context

AP-2:包装已包含上下文的错误

What it looks like:
fmt.Errorf("parse error: %w", strconv.ParseUint(...))
Why wrong: strconv already includes function name, input, and error type Do instead:
must.Return(strconv.ParseUint(s, 10, 32))
表现
fmt.Errorf("parse error: %w", strconv.ParseUint(...))
错误原因:strconv已包含函数名、输入和错误类型 正确做法
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
args[0]
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
表现:使用switch语句基于
args[0]
分发到不同代码路径 错误原因:Cobra子命令能以更好的用户体验处理此问题 正确做法:如有需要,修改参数顺序以支持Cobra子命令

AP-4: Using must.Return in Request Handlers

AP-4:在请求处理器中使用must.Return

What it looks like:
val := must.Return(someOperation())
inside an HTTP handler Why wrong:
must.Return
calls
os.Exit(1)
on error, crashing the server Do instead: Return errors properly;
must.*
is for startup code and tests only
表现:在HTTP处理器中使用
val := must.Return(someOperation())
错误原因
must.Return
在错误时会调用
os.Exit(1)
,导致服务器崩溃 正确做法:正确返回错误;
must.*
仅用于启动代码和测试

AP-5: Global Mutable Variables for Configuration

AP-5:使用全局可变变量存储配置

What it looks like:
var Config = map[string]string{...}
at package level Why wrong: Callers can modify the map, creating inconsistent state Do instead: Functions that produce values:
func GetConfig() map[string]string

表现:包级别变量
var Config = map[string]string{...}
错误原因:调用者可以修改该映射,导致状态不一致 正确做法:使用生成值的函数:
func GetConfig() map[string]string

Available Scripts

可用脚本

Deterministic checks for sapcc-specific patterns that no linter covers. Run these during code review or as part of quality gates. All support
--help
,
--json
,
--limit
, and meaningful exit codes (0 = clean, 1 = violations, 2 = error).
ScriptWhat It Checks
scripts/check-sapcc-identify-endpoint.sh
HTTP handlers missing
httpapi.IdentifyEndpoint
call
scripts/check-sapcc-auth-ordering.sh
Data access before authentication in handlers
scripts/check-sapcc-json-strict.sh
json.NewDecoder
without
DisallowUnknownFields()
scripts/check-sapcc-time-now.sh
Direct
time.Now()
in testable code (inject clock instead)
scripts/check-sapcc-httptest.sh
httptest.NewRecorder
instead of
assert.HTTPRequest
scripts/check-sapcc-todo-format.sh
Bare TODO comments without context/links
These scripts only apply to sapcc repos (detected by
github.com/sapcc/go-bits
in go.mod).

用于检测特定sapcc模式的确定性检查,这些模式没有检查器覆盖。在代码评审期间或作为质量门运行这些脚本。所有脚本都支持
--help
--json
--limit
和有意义的退出码(0 = 无问题,1 = 违反规则,2 = 错误)。
脚本检查内容
scripts/check-sapcc-identify-endpoint.sh
HTTP处理器缺少
httpapi.IdentifyEndpoint
调用
scripts/check-sapcc-auth-ordering.sh
处理器中数据访问在认证之前
scripts/check-sapcc-json-strict.sh
json.NewDecoder
未使用
DisallowUnknownFields()
scripts/check-sapcc-time-now.sh
可测试代码中直接使用
time.Now()
(应注入时钟)
scripts/check-sapcc-httptest.sh
使用
httptest.NewRecorder
而非
assert.HTTPRequest
scripts/check-sapcc-todo-format.sh
无上下文/链接的裸TODO注释
这些脚本仅适用于sapcc仓库(通过go.mod中的
github.com/sapcc/go-bits
检测)。

Anti-Rationalization

反合理化

SAP CC Domain-Specific Rationalizations

SAP CC领域特定的合理化反驳

RationalizationWhy It's WrongRequired Action
"Tests pass, the error wrapping is fine"Lead review checks error message quality in reviewVerify error context matches project standards
"Copilot suggested this approach"Lead review frequently rejects Copilot suggestions as overengineeredEvaluate on merit, simplify where possible
"I need a struct for this JSON"One-off JSON can be
fmt.Sprintf
+
json.Marshal
Only create types if reused or complex
"Better safe than sorry" (re: error handling)"Irrelevant contrivance" -- handle only practical concernsAsk "concrete scenario where this fails?"
"Standard library X works fine here"SAP CC has go-bits equivalents that are expectedUse go-bits (logg, assert, must, osext, respondwith)
"testify is the Go standard"SAP CC projects use go-bits/assert exclusivelyNever introduce testify in sapcc repos
"I'll add comprehensive error wrapping"Trust well-designed functions' error messagesCheck if the called function already provides context
"This needs a config file"SAP CC uses env vars onlyUse osext.MustGetenv, GetenvOrDefault, GetenvBool

合理化理由错误原因要求操作
"测试通过,错误包装没问题"主评审会在评审中检查错误消息质量验证错误上下文是否符合项目标准
"Copilot建议了这种方法"主评审经常拒绝Copilot的过度工程建议根据实际价值评估,尽可能简化
"我需要一个结构体来处理这个JSON"一次性JSON可以使用
fmt.Sprintf
+
json.Marshal
仅在复用或复杂时创建类型
"谨慎总比抱歉好"(关于错误处理)被视为“无关紧要的矫揉造作”——仅处理实际问题问自己“这个方案会失败的具体场景是什么?”
"标准库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):
  1. sapcc-code-patterns.md — Read FIRST. This is the definitive reference with actual code patterns
  2. library-reference.md — Read SECOND. Know which libraries are approved/forbidden before writing imports
  3. architecture-patterns.md — Read THIRD if working on architecture, HTTP handlers, or DB access
  4. Then load others as needed for the specific task
FileWhat It ContainsWhen to Read
references/sapcc-code-patterns.mdActual code patterns — function signatures, constructors, interfaces, HTTP handlers, error handling, DB access, testing, package organizationALWAYS — this is the primary reference
references/library-reference.mdComplete library table: 30 approved, 10+ forbidden, with versions and usage countsALWAYS — need to know approved/forbidden imports
references/architecture-patterns.mdFull 102-rule architecture specification with code examplesWhen working on architecture, handlers, DB access
references/review-standards-lead.mdAll 21 lead review comments with full context and quotesFor reviews and understanding lead review reasoning
references/review-standards-secondary.mdAll 15 secondary review comments with PR contextFor reviews and understanding secondary review patterns
references/anti-patterns.md20+ SAP CC anti-patterns with BAD/GOOD code examplesFor code review and avoiding common mistakes
references/extended-patterns.mdExtended 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评审的真实规则。
阅读顺序(按此顺序阅读):
  1. sapcc-code-patterns.md —— 首先阅读。这是包含实际代码模式的权威参考
  2. library-reference.md —— 其次阅读。在编写导入之前,了解哪些库是批准/禁止的
  3. architecture-patterns.md —— 如果处理架构、HTTP处理器或数据库访问,第三阅读
  4. 然后根据具体任务需要加载其他文件
文件内容阅读时机
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.md20+个SAP CC反模式,包含错误/正确代码示例进行代码评审或避免常见错误时
references/extended-patterns.md来自相关仓库的扩展模式 —— 安全微模式、可视化章节分隔符、版权格式、K8s命名空间隔离、PR卫生(排序列表、清理孤立代码、随代码文档化)、变更日志格式。由流水线生成。处理安全相关代码、K8s helm工作或PR卫生时