《互联网产品的服务端架构·产品、流量与基础能力》附录 B Go 实验工程与代码分级

附录 B 记录 webapp-lab 的真实工程快照:Go 版本、go.mod、全部 Go 文件清单、目录与包职责、L1 到 L4 的验证分级、三行代码头与 verify 约定,以及只跑一个包、按依赖顺序读代码的方法,帮助读者复现书中的每一个代码块,并说明本卷做到哪一级。

先确认工程,而不是先讲概念

读代码之前,先确认三件事:工程在哪里、用什么版本、里面有哪些文件。这三件事看起来琐碎,却能避免大部分“代码跑不起来”的问题。

本书的配套工程叫 webapp-lab,模块名 webapp。它不依赖外部框架,也不引用第三方模块,所有代码都使用 Go 标准库。这样做的目的不是排斥框架,而是让读者把注意力放在判断和边界上:当一段代码只有标准库时,读者更容易看清它到底在解决什么问题,而不是被框架的配置层遮住。

工程在写作工作区里的位置是 .local/checks/webapp-lab。这个路径属于本书的临时工作区,不是永久发布地址;如果你拿到的是独立副本,以副本的实际路径为准。下面的命令都假设当前目录是工程根目录。

工程快照:文件、版本与模块

下面的输出来自一次真实运行。运行时间是 2026-11-04 13:58:43 CST,机器是 darwin/arm64。工程在写作过程中还会增补文件,因此这份快照只代表该时刻;读者应以自己拿到的工程为准,但可以用同样的命令重新确认。

$ date "+%Y-%m-%d %H:%M:%S %Z"
2026-11-04 13:58:43 CST

$ find . -type f -name "*.go" | sort
./internal/account/security_test.go
./internal/account/security.go
./internal/account/session_test.go
./internal/account/session.go
./internal/account/signup_test.go
./internal/account/signup.go
./internal/api/errors_test.go
./internal/api/errors.go
./internal/api/semantics_test.go
./internal/api/semantics.go
./internal/api/version_test.go
./internal/api/version.go
./internal/async/event_test.go
./internal/async/event.go
./internal/async/mode_test.go
./internal/async/mode.go
./internal/async/retry_test.go
./internal/async/retry.go
./internal/baseline/e2e_test.go
./internal/baseline/e2e.go
./internal/baseline/exit_test.go
./internal/baseline/exit.go
./internal/baseline/module_test.go
./internal/baseline/module.go
./internal/bff/aggregate_test.go
./internal/bff/aggregate.go
./internal/bff/client_test.go
./internal/bff/client.go
./internal/bff/tradeoff_test.go
./internal/bff/tradeoff.go
./internal/cache/hotkey_test.go
./internal/cache/hotkey.go
./internal/cache/profile_test.go
./internal/cache/profile.go
./internal/cache/window_test.go
./internal/cache/window.go
./internal/edge/gateway_test.go
./internal/edge/gateway.go
./internal/edge/origin_test.go
./internal/edge/origin.go
./internal/edge/static_test.go
./internal/edge/static.go
./internal/obs/incident_test.go
./internal/obs/incident.go
./internal/obs/signals_test.go
./internal/obs/signals.go
./internal/obs/sli_test.go
./internal/obs/sli.go
./internal/platform/capacity/capacity_test.go
./internal/platform/capacity/capacity.go
./internal/platform/capacity/cost_test.go
./internal/platform/capacity/cost.go
./internal/platform/capacity/elastic_test.go
./internal/platform/capacity/elastic.go
./internal/platform/capacity/sensitivity_test.go
./internal/platform/capacity/sensitivity.go
./internal/platform/capacity/storage_test.go
./internal/platform/capacity/storage.go
./internal/platform/design/adr_test.go
./internal/platform/design/adr.go
./internal/platform/design/ownership_test.go
./internal/platform/design/ownership.go
./internal/platform/design/usecase_test.go
./internal/platform/design/usecase.go
./internal/platform/product/shape_test.go
./internal/platform/product/shape.go
./internal/platform/slo/slo_test.go
./internal/platform/slo/slo.go
./internal/profile/deletion_test.go
./internal/profile/deletion.go
./internal/profile/pii_test.go
./internal/profile/pii.go
./internal/profile/visibility_test.go
./internal/profile/visibility.go
./internal/release/rollout_test.go
./internal/release/rollout.go
./internal/release/schema_test.go
./internal/release/schema.go
./internal/release/unit_test.go
./internal/release/unit.go

$ go version
go version go1.26.0 darwin/arm64

$ cat go.mod
module webapp

go 1.26.0

$ go test ./...
ok  	webapp/internal/account	(cached)
ok  	webapp/internal/api	(cached)
ok  	webapp/internal/async	(cached)
ok  	webapp/internal/baseline	0.264s
ok  	webapp/internal/bff	(cached)
ok  	webapp/internal/cache	(cached)
ok  	webapp/internal/edge	(cached)
ok  	webapp/internal/obs	(cached)
ok  	webapp/internal/platform/capacity	(cached)
ok  	webapp/internal/platform/design	(cached)
ok  	webapp/internal/platform/product	(cached)
ok  	webapp/internal/platform/slo	(cached)
ok  	webapp/internal/profile	(cached)
ok  	webapp/internal/release	0.455s

几点说明:

  • 输出中的 (cached) 表示该包之前已经运行过,Go 复用了测试缓存。首次运行会显示实际耗时。
  • 耗时只是一次本机运行的结果,随机器负载变化,不作为性能承诺。
  • 文件清单包含实现文件与测试文件,共 80 个 Go 文件,分布在 14 个包中。
  • go test ./... 通过只说明这些包的测试在当前环境通过,不说明真实生产环境可用。

目录与包职责

工程按“平台能力”和“产品能力”分成两层。平台能力放在 internal/platform/ 下,产品能力放在 internal/ 下的其他包。这个划分不是微服务边界,而是阅读和依赖的边界:平台包尽量不依赖产品包,产品包可以依赖平台包。

internal/
├── account/          账号、会话、安全
├── api/              API 语义、错误码、版本
├── async/            异步模式、事件、重试
├── baseline/         模块装配、端到端、基线对比
├── bff/              端能力、聚合、裁剪
├── cache/            流量画像、缓存窗口、热 key
├── edge/             静态动态分类、网关、回源
├── obs/              SLI、信号审计、故障时间线
├── platform/
│   ├── capacity/     容量、区间、成本、存储、弹性
│   ├── design/       用例、所有权、ADR
│   ├── product/      产品形状分类
│   └── slo/          错误预算、burn rate
├── profile/          可见性、PII 最小化、注销
└── release/          发布单元、灰度、数据兼容

每个包的职责与卷一章节的对应关系如下:

包主要文件职责本书用途
internal/platform/productshape.go按实时、强一致、读写比、写多分类第 1 章产品形状
internal/platform/sloslo.go错误预算与 burn rate第 1、11 章
internal/platform/capacitycapacity.go、sensitivity.go、cost.go、storage.go、elastic.goQPS、区间、成本、存储、弹性第 1、12 章
internal/platform/designusecase.go、ownership.go、adr.go用例边界、数据所有权、决策记录第 2 章
internal/accountsignup.go、session.go、security.go第三方身份、会话设备、登录风控第 3 章
internal/profilevisibility.go、pii.go、deletion.go资料可见性、PII 最小化、注销导出第 4 章
internal/bffclient.go、aggregate.go、tradeoff.go端能力、聚合、载荷裁剪第 5 章
internal/apisemantics.go、errors.go、version.go资源命令语义、错误码、版本兼容第 6 章
internal/edgestatic.go、gateway.go、origin.go静态动态、网关路由、回源保护第 7 章
internal/cacheprofile.go、window.go、hotkey.go流量画像、一致性窗口、热 key第 8 章
internal/asyncmode.go、event.go、retry.go同步异步判断、事件去重、重试补偿第 9 章
internal/releaseunit.go、rollout.go、schema.go发布单元、灰度回滚、双写兼容第 10 章
internal/obssli.go、signals.go、incident.goSLI 计算、观测信号、故障时间线第 11 章
internal/baselinemodule.go、e2e.go、exit.go模块校验、端到端、基线对比第 13 章

包名和章节不是一一对应的行政划分。一个包可能服务多章,一个章节也可能引用多个包。例如 capacity 同时服务第 1 章的规模曲线和第 12 章的成本估算;第 13 章则把 account、profile、bff 和 baseline 串起来。

L1 到 L4 的分级

本书用四级描述代码能证明到什么程度。级别越高,越接近真实环境,但复现成本也越高。

级别判定验收方式本卷落点
L1纯逻辑或单包,不依赖外部进程gofmt -l 无输出、go vet、go test,并发包加 -race绝大多数章节
L2进程内端到端,httptest 或本机端口,内存适配器一个脚本跑通产品用例并断言状态第 13 章
L3真实依赖,例如 Docker Compose + PostgreSQL固定场景脚本与原始输出,只写实际跑过的数字本卷可选
L4多节点、故障注入、网络分区真实环境演练与记录本卷不做

卷一的要求是:所有章节至少 L1;账号、资料与 BFF 有一条 L2 用例;如果环境允许,补一次 L3 端到端。L4 不在本卷范围内,涉及多节点容灾的结论会明确标注“需真实环境验证”。

这个分级最重要的作用是防止过度声称。一个 L1 测试通过,只说明函数在给定输入下行为正确;它不能证明数据库连接池够用、网络分区时不会丢消息、第三方支付回调不会重复。把 L1 当成生产可用,是读代码时最容易犯的错误。

反过来,也不能因为“只是 L1”就轻视它。本书的很多关键判断都固定在 L1 里:分类优先级、错误预算公式、幂等去重、版本兼容规则。这些判断一旦写错,后面的 L2、L3 只会把错误放大。L1 是地基,不是演示。

三行头与 verify 约定

每个代码块的开头有三行固定头部:

// file: internal/platform/product/shape.go
// tier: L1
// verify: go test ./internal/platform/product/

三行的含义:

  • file:代码在工程中的相对路径。按这个路径能找到完整文件。
  • tier:验证级别,取值 L1 到 L4。它告诉读者需要什么环境。
  • verify:最小验证命令。复制到工程根目录执行,应该能复现代码块声称的结果。

代码块的内容与文件逐字节一致,包括注释、空行和测试文件。这样做的原因很实际:技术书里最常见的信任崩塌,是书里的代码能跑、复制下来不能跑。逐字节一致至少能保证读者看到的不是“整理过的版本”。

如果代码块来自测试文件,file 会指向 _test.go,verify 仍然是对应包的测试命令。读者可以把实现和测试放在一起看,理解作者为什么选择这些边界用例。

三行头不是装饰,也不建议省略。没有 file,读者不知道代码属于哪个包;没有 tier,读者不知道它需要什么环境;没有 verify,读者只能相信作者的口头结论。这三行是代码可复现的最小契约。

如何只跑一个包

不需要每次跑全仓。只验证一个包,通常更快,也更容易定位问题。

在工程根目录执行:

  • go test ./internal/platform/product/:运行产品形状包的全部测试。
  • go test -run TestClassifyPriority ./internal/platform/product/:只运行一个测试函数。
  • go test -v ./internal/account/:显示每个测试的名称和结果。
  • go test -race ./internal/account/:开启竞态检测,适合会话和并发相关的包。
  • go test -count=1 ./...:跳过测试缓存,强制重新运行。
  • go vet ./internal/profile/:只对资料包做静态检查。

如果只想确认格式,使用 gofmt -l <目录>。没有输出表示该目录下的文件都符合格式;有输出时,列出的文件需要运行 gofmt -w 修正。

跑单包时要注意依赖。Go 的包测试只编译该包及其依赖,不会自动运行依赖包的测试。因此 go test ./internal/bff/ 通过,不代表 internal/account 也通过;反过来,某个底层包失败可能会让上层包编译失败。定位问题时,先看失败信息里的包路径。

按依赖顺序读代码

工程的包很多,不建议从文件列表第一行开始读。推荐的顺序是:

  1. internal/platform/product:先理解产品形状分类,这是全书判断的起点。
  2. internal/platform/slo 和 internal/platform/capacity:再看可用性底线与容量模型。
  3. internal/platform/design:然后看用例、所有权和 ADR,理解边界怎么表达。
  4. internal/account 和 internal/profile:进入身份与资料,观察状态机和可见性规则。
  5. internal/bff 和 internal/api:看多端聚合、错误码和版本兼容。
  6. internal/edge、internal/cache、internal/async:看入口、读路径和异步边界。
  7. internal/obs 和 internal/release:最后看观测、发布、灰度与数据兼容。
  8. internal/baseline:把前面的模块装配成端到端用例,做基线对比。

这个顺序大致对应卷一的章节顺序,但不必严格一致。如果你正在做账号相关的工作,可以先读 account 和 profile,再回头补 platform 的约束。只要记住一点:后面的包通常假设前面的判断已经成立。

读每个包时,建议先看导出的类型和函数签名,再看测试文件。测试文件往往比实现更能说明边界:作者选择了哪些非法输入、哪些状态迁移、哪些重复执行场景,这些选择就是包的设计意图。

测试文件怎么读

本书的测试文件通常比实现文件长,因为边界比主路径更重要。读测试时,可以按下面的顺序。

先看测试函数名。TestClassifyPriority、TestLinkIsIdempotentForSameAccount、TestAdvanceRejectsInvalidTransitions 这样的名字,已经说明了被测行为和预期结果。把同一包的测试名连起来读,能快速看出作者认为哪些边界值得保护。

再看表驱动结构。很多测试用一个切片列出多组输入和期望输出,每组包含正常值、边界值和非法值。表驱动的好处是新增用例只需要加一行,缺点是失败时只看行号不容易定位;因此作者通常会给每组用例加一个名字。

然后看断言。好的断言不仅检查返回值,还检查副作用、错误类型和输入是否被修改。例如缓存窗口测试会检查原始切片没有被排序或改写,资料过滤测试会检查空输入返回的是空切片而不是 nil。这些细节是包契约的一部分。

最后看没有测什么。测试文件不会覆盖所有情况,作者选择的边界就是他认为最重要的风险。如果某个分支没有测试,读实现时就要多留意它。

失败信息怎么读

测试失败时,先读三行:失败的测试名、失败的文件和行号、断言输出的期望值与实际值。大多数问题能在这三行里定位。

如果失败发生在编译阶段,先看包路径和符号名。常见原因是包正在修改、接口签名变化或测试文件与实现文件不同步。这时不要急着改测试,先确认工程当前状态。

如果失败发生在断言阶段,先判断是输入问题还是实现问题。把失败用例的输入代入手算一遍,看看期望值是否合理;如果期望值本身错了,修测试;如果期望值合理,修实现。

如果失败只在 -race 下出现,说明存在并发访问问题。先看共享变量是否被多个 goroutine 同时读写,再看锁的粒度和生命周期。竞态问题不能靠重跑解决,必须找到具体的读写冲突。

如果失败只在 -count=1 下出现,说明之前的结果来自缓存,代码可能已经变了。以非缓存结果为准,并检查是否有文件在两次运行之间被修改。

一个包的阅读模板

第一次打开一个包,可以按固定模板走一遍,避免从第一行读到最后一行却抓不住重点。

  1. 先读包注释和导出类型,知道这个包对外承诺什么。
  2. 再读导出函数的参数和返回值,尤其是错误返回的条件。
  3. 然后读实现里的 switch、if err != nil 和边界检查,这些地方通常藏着设计判断。
  4. 接着读测试名,确认作者认为哪些行为必须被保护。
  5. 最后读测试用例的输入,看看非法值、重复值和边界值是怎么构造的。

这个模板适用于本书的大多数包。它不能替代完整阅读,但能让你在十分钟内判断一个包是否值得深入,以及它和当前章节的关系。

如果一个包看完仍然不知道它解决什么问题,回到文章对应小节,先读产品场景,再回来看代码。代码是答案的一部分,不是问题本身。

小结

附录 B 给出的是工程事实,不是概念介绍。webapp-lab 使用 Go 1.26.0、模块 webapp,当前快照有 80 个 Go 文件、14 个包;代码按 L1 到 L4 分级,卷一主要做到 L1,第 13 章有一条 L2 用例;每个代码块有三行头,verify 是最小复现命令。

读代码时,先确认版本和目录,再按包运行测试,最后按依赖顺序阅读。不要把 L1 通过当成生产可用,也不要把全仓通过当成单包正确。工程是论证工具,不是脚手架;它的价值在于让书里的判断可以被检查,而不是替读者做架构决策。

常见错误与边界

  • 在错误目录运行命令。所有相对路径都基于 webapp-lab 根目录,不在根目录执行会找不到包。
  • 忽略 Go 版本。书里的代码验证于 Go 1.26.0,其他版本可能有标准库行为差异。
  • 把 (cached) 当成没有真正运行。缓存命中表示之前运行过;需要强制重跑时加 -count=1。
  • 把 go test ./... 通过当成生产可用。它只证明测试环境通过,不证明真实依赖和故障场景可靠。
  • 跳过测试文件。测试是包的设计说明,尤其是非法输入和状态迁移部分。
  • 修改代码块却不改工程文件。书里的代码与工程逐字节一致,单方面修改会让 verify 失去意义。
  • 把 L1 包当成微服务边界。这些包是阅读和依赖边界,不是部署单元。
  • 认为文件清单永远不变。工程会增补,快照只代表运行时刻;用同样的命令重新确认即可。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查