此前,我曾经整理过一篇文章,初步讨论如何开发一个能够长期维护的 A Tour of Go 简体中文版。
相关文章:
当时的设想还比较宽泛,包括:
- 使用 Go 开发项目;
- 使用 Gin 提供管理能力;
- 调用 GLM-5.2 翻译课程内容;
- 建立翻译状态和同步机制;
- 最终部署一个可以公开访问的中文 Tour。
经过几天对官方源码、课程结构、生产运行方式和翻译流程的进一步分析,这个项目的设计已经逐渐收敛。
截至 2026 年 8 月 1 日,页面级翻译契约和实现准备清单都已经冻结。
下一步不再继续扩展设计,而是正式创建 go-tour-i18n 仓库,从第一个最小开发任务开始实施。
一、最初确认的 101 页并不是完整生产页面数
在最开始检查官方源码时,我统计了 _content/tour 目录下的七个 .article 文件。
直接按照原始 article 中的顶层 Section 统计,共有 101 个课程页面。
本地进入官方 tour 目录以后执行:
go run .
看到的同样是 101 页。
其中 welcome.article 在本地显示三个页面:
- Hello, 世界
- Go local
- Congratulations
但是当前官方 A Tour of Go 网站中的 Welcome 实际上包含五页:
- Hello, 世界
- Go local
- Go offline(可选)
- The Go Playground
- Congratulations
这说明本地直接启动 Tour 得到的页面结构,与官方生产环境并不完全一致。
二、101 页和 103 页来自两个不同的页面投影
继续分析官方源码后,最终确认:
| 统计方式 | 页面数 |
|---|---|
| Raw article 显式 Section | 101 |
| Standalone 本地运行 | 101 |
| Integrated production | 103 |
差异主要来自 welcome.article 中的 #appengine: 条件内容。
官方生产环境在解析 article 之前,会先执行一层类似 gaePrepContent 的预处理。
这层预处理会:
- 去掉
#appengine:前缀; - 保留生产环境需要的标题、段落和指令;
- 删除与其对应的 standalone 替代内容;
- 将条件
.play、命令和标题转换成真正的 present 内容。
因此,下面两页只会出现在生产投影中:
- Go offline(可选)
- The Go Playground
最终当前 upstream 基线的页面数量为:
| Lesson | 页数 |
|---|---|
| basics | 17 |
| concurrency | 11 |
| flowcontrol | 14 |
| generics | 3 |
| methods | 26 |
| moretypes | 27 |
| welcome | 5 |
| 总计 | 103 |
这里的 103 只属于当前固定的 upstream commit:
e11dacba76c5aae474746e9eedee19693f492803
它不能成为业务代码中的永久常量。
未来官方新增、删除、拆分或者合并页面后,合法页面数完全可能变成 104 或其他数字。
项目必须根据当前 upstream 动态生成 production manifest,再用 manifest 判断页面数量、顺序和前后关系。
三、翻译单元最终确定为 Production 页面
最开始,我曾经把 raw article 中的顶层 Section 视为翻译单元。
这种设计在 101 页和 103 页差异被发现后已经不再成立。
现在正式冻结的定义是:
对 raw article 执行 integrated production 预处理,再使用固定版本的
present.Context.Parse解析;最终产生的顶层present.Section,才是一个完整的翻译页面。
这意味着:
- Go local 是一个翻译请求;
- Go offline 是另一个翻译请求;
- The Go Playground 又是一个独立请求。
不能因为它们在原始源码中存在条件关系,就把它们合并成同一个翻译任务。
页面翻译流程已经固定为:
完整 Production 页面
→ GLM-5.2 整页翻译
→ 保存 Candidate
→ 自动结构和页面校验
├─ 通过 → Ready → 显式发布
└─ 失败 → 有限整页重试 → Blocked
↓
ChatGPT 整页翻译
↓
同一套自动校验
这里不会采用:
- 段落级独立翻译;
- AST 节点级翻译;
- JSON 多个
text字段; - 页面失败后自动拆分;
- 常规逐页人工审批。
模型始终一次接收一个完整课程页面,以保留完整上下文。
四、普通段落结构也需要自动保护
整页翻译并不代表译文可以任意改变页面结构。
冻结后的规则要求:
- 普通段落数量默认保持一致;
- 段落顺序必须一致;
- 段落所在的结构位置必须一致;
- 同一段落内部可以调整中文语序;
- 同一段落内部可以拆分或合并句子;
- 默认不能跨段合并;
- 不能删除或新增段落;
- 不能把段落移动到列表或其他章节位置。
如果发生这种变化,校验器会返回:
paragraph_structure_changed
只有 manifest 中存在绑定当前 page_id 和 source_content_hash 的精确例外时,才允许改变某个页面的段落边界。
这样既保留了整页翻译的上下文优势,又防止模型在不知不觉中改变课程结构。
五、工作流状态和线上发布版本必须独立
另一个重要决定,是将当前翻译状态与线上发布版本完全分开。
例如官方页面更新以后,可以同时存在:
translation:
status: needs_retranslation
publication:
published_version: 4
它表示:
- 官方英文页面已经更新;
- 新的中文译文尚未完成;
- 旧的中文 version 4 仍然正常在线。
下面这些状态都不能清除旧的线上版本:
needs_retranslationtranslatingtranslation_failedtranslatedvalidation_failedblocked
只有一个已经进入 ready 的 Candidate,经过显式发布操作以后,才允许移动 published_version 指针。
这样可以避免一次翻译失败导致原本正常的中文页面下线。
六、曾经计划使用 Gin,最终决定取消
在最早的项目设计中,我曾经希望使用 Gin。
一方面,这是一个 Go 实战项目,我希望尽量练习 Go 生态中的常见框架。
另一方面,我最初以为 Gin 会像 Yii 2 一样,同时提供 Web 和 CLI 两种运行模式。
Yii 2 中可以分别使用:
- Web Application;
- Console Application。
但是进一步确认后发现,Gin 本质上是一个 HTTP Web 框架,并没有天然的 Console 模式。
如果坚持使用 Gin,同时又需要 CLI,通常有几种方式:
- CLI 和 Gin 共享同一套核心业务服务;
- CLI 作为客户端调用 Gin API;
- 使用 Cobra 等工具单独实现 CLI;
- Gin 只在 Web 服务启动时使用。
一度考虑过这样的结构:
CLI
→ Gin 管理 API
→ 核心应用服务
未来可能存在的后台管理界面,也通过同一套 Gin API 管理翻译、校验和发布。
但是继续分析以后,我发现这个项目很可能永远不需要 Web 管理界面。
整个工作流主要是:
- 获取 upstream;
- 生成 manifest;
- 翻译页面;
- 自动校验;
- 导出 blocked 页面;
- 导入 ChatGPT 译文;
- 生成 article;
- 发布和回滚。
这些都是非常典型的命令行批处理任务。
我此前处理 WordPress 历史文章翻译、摘要补全和 SyntaxHighlighter 迁移时,也一直主要使用 CLI。
实际使用下来,CLI 已经足够完成:
- 固定批次;
- 状态检查;
- 有限重试;
- 失败恢复;
- 只读验证;
- 批量执行;
- 日志留存。
如果为了一个可能永远不会出现的后台界面提前引入 Gin,反而会增加:
- HTTP API 设计;
- 服务启动和关闭;
- API 认证;
- 请求超时;
- 长任务状态;
- CLI HTTP 客户端;
- 服务端和客户端错误码;
- 本地和 CI 对常驻服务的依赖。
因此,项目最终决定:
第一阶段放弃 Gin,也不再规划 Web 管理界面。
七、正式选择 Go 加 Cobra
项目使用 Go 语言实现这一点没有变化。
CLI 框架正式选择 Cobra。
最终关系是:
Cobra CLI
→ Application Service
→ Upstream、Translation、Validation、Workspace
Cobra 只负责:
- 命令和子命令;
- 参数与 Flag;
- 帮助信息;
- Shell 补全;
- 调用应用服务;
- 输出结果;
- 返回退出码;
- 传递 Context 和取消信号。
真正的业务逻辑不会写进 Cobra 的 RunE。
例如下面这些行为都属于独立应用服务:
BuildManifestTranslatePageValidatePageExportBlockedPageImportCandidateGenerateLocalePublishLocaleRollbackLocale
预计最终命令形式包括:
go-tour-i18n upstream verify
go-tour-i18n manifest build
go-tour-i18n manifest show
go-tour-i18n page translate
go-tour-i18n page validate
go-tour-i18n blocked export
go-tour-i18n candidate import
go-tour-i18n locale generate
go-tour-i18n locale publish
go-tour-i18n locale rollback
go-tour-i18n preview
go-tour-i18n status
第一阶段不会一次实现所有命令,而是随着项目阶段逐步增加。
八、独立仓库不能直接导入官方 internal/tour
新的项目仓库暂定为:
go-tour-i18n
但官方 Tour 的核心实现位于:
golang.org/x/website/internal/tour
Go 的 internal 规则决定了,独立仓库不能直接导入这个包。
因此不能直接调用:
gaePrepContentparseLessoninitTour- 官方未导出的 Handler
冻结后的方案是:
管理和构建侧
在 go-tour-i18n 中实现一个非常小的、带版本号的 gaePrepContent 等价 projector。
它只实现当前项目真正需要的语义,并通过 golden test 与固定 upstream 行为对比。
预览和运行侧
不重写官方 Tour 服务。
项目会在临时 staging 目录中:
- 取得锁定的官方 website 源码;
- 写入生成后的中文
.article; - 应用少量 UI 和运行配置 Patch;
- 构建或启动官方 Tour 兼容服务;
- 退出后清理 staging。
这样既避免复制完整官方 Tour,又不会修改 upstream 缓存目录。
九、Upstream 使用 Lock 文件固定
项目不会依赖我当前已经存在的:
~/code/go-website-upstream
正式方案采用:
upstream.lock
+ CLI 管理的本地缓存 Checkout
upstream.lock 会记录:
- 官方仓库地址;
- 固定 commit;
- projector contract;
appengine.go的 hash;present模块和版本;- 课程顺序来源文件和 hash。
本地、CI 和以后发布构建,都必须根据同一份 Lock 获取源码。
当前本地的 ~/code/go-website-upstream 只用于初始分析和方便阅读源码,不能成为项目构建成立的隐含条件。
十、测试采用两层 Fixture
冻结前还发现了一个测试设计问题。
最初首个任务只打算保存 welcome.article,但同时又希望验证完整的 101 和 103 页统计。
单个 Welcome 文件显然无法验证七个 Lesson 的完整页面数。
最终改为两层 Fixture。
Projector 小型 Fixture
只验证逐行预处理行为,例如:
#appengine:条件替代;- 条件标题;
- 缩进命令;
- 条件
.play; - 条件 Note。
它们规模很小,适合快速单元测试。
完整 Upstream 基线 Fixture
保存当前 commit 下的七个 .article 文件,用于离线生成完整 manifest。
它必须验证:
- Raw 总计 101;
- Integrated 总计 103;
- 每个 Lesson 的页面数;
- 官方课程顺序;
- Lesson 内页面顺序;
- Previous 和 Next 关系。
这些基线数字只会出现在固定 commit 的测试期望中,不会进入业务代码。
十一、部署方式暂时不提前确定
实现准备阶段曾经一度倾向于把容器镜像作为默认发布方式。
但当前服务器已经有:
- Linux ECS;
- Nginx;
- 现有服务管理和运维环境;
- EdgeOne。
对于一个单独的 Go Tour 服务,最终最简单的方案可能是:
Linux 二进制
+ systemd
+ 现有 Nginx
+ EdgeOne
也可能在实际测试后发现容器更合适。
因此,现在只确定第一阶段 Release 必须提供:
- 可独立运行的 Linux amd64 二进制;
- Release Manifest;
- Artifact Hash;
- License 和第三方声明;
- 运行配置说明。
systemd 和容器在第五阶段根据实际环境再比较,不同时维护两套复杂部署方案。
十二、中文站点域名也已经调整
最初暂定的中文域名是:
zh.go-tour.shuijingwanwq.com
现在调整为:
https://go-tour.shuijingwanwq.com
默认语言就是简体中文,因此不使用 /zh/ 前缀。
例如:
https://go-tour.shuijingwanwq.com/tour/welcome/1
https://go-tour.shuijingwanwq.com/tour/basics/1
未来增加其他语言时,可以使用:
/ja/tour/...
/de/tour/...
默认中文继续使用无语言前缀地址。
不会同时公开:
/tour/welcome/1
/zh/tour/welcome/1
避免产生重复页面。
十三、下一个任务只验证 Upstream 和 Manifest
今天已经完成了方案冻结,暂时不继续创建仓库。
明天开始时,第一个开发任务会严格限制范围:
- 创建
go-tour-i18n本地仓库; - 初始化 Go Module;
- 接入 Cobra;
- 创建
upstream.lock; - 实现最小等价 projector;
- 生成 raw-to-output provenance;
- 使用
golang.org/x/tools/present v0.48.0; - 生成 integrated production manifest;
- 实现三个最小命令:
upstream verifymanifest buildmanifest show
- 完成小型 projector Fixture 和完整基线 Fixture 测试。
当前任务不会:
- 调用 GLM-5.2;
- 翻译任何页面;
- 实现 Candidate;
- 实现状态机;
- 实现 Blocked;
- 实现发布;
- 实现回滚;
- 实现 Preview;
- 部署测试站。
第一步只需要证明:
项目能够从锁定的官方源码中,稳定、离线、可重复地生成正确的 Integrated Production Manifest。
十四、设计阶段终于可以结束
这几天最大的变化,并不是增加了多少功能,而是逐渐删除了不必要的设计。
被删除或推迟的内容包括:
- Gin;
- Web 管理 API;
- Web 管理界面;
- 数据库;
- 逐页人工审核;
- 运行时动态语言切换;
- 多语言同时上线;
- 复杂任务队列;
- 分布式翻译;
- 自建公网代码沙箱;
- 同时维护容器和 systemd 两套发布流程。
最终留下来的,是更符合当前真实需求的一条路线:
Go + Cobra
→ 锁定 Upstream
→ Production 页面投影
→ 整页翻译
→ 自动校验
→ 文件状态
→ 官方 Tour 兼容生成与发布
这个设计依然具备多语言扩展能力,但第一阶段只完成 zh-CN。
它也仍然是一个完整的 Go 实战项目,只是不再为了使用某个框架而人为增加 Web 管理系统。
接下来要做的事情已经非常明确。
明天开始创建 go-tour-i18n 仓库,从 upstream、projector 和 production manifest 开始,逐步把已经冻结的设计真正实现出来。
需要长期技术维护或远程问题排查?
我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。
如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:
- ✅ PHP / Laravel / Yii2 老项目无人维护
- ✅ Go / Gin 后端接口需要排查或优化
- ✅ WordPress 网站访问慢、报错或插件冲突
- ✅ Nginx / MySQL / Redis / Linux 服务器异常
- ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
- ✅ 需要长期远程技术支持或兼职维护
更多介绍请查看:关于我 & 合作
微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan


发表回复