上一篇文章中,我完成了 A Tour of Go 中文版项目的设计冻结,重点讨论了页面数量、翻译单元、状态流程和命令行架构等问题。(Shuijing Wanwq)
设计冻结以后,我并没有立即调用 GLM-5.2 开始翻译。
真正进入实现阶段后,我首先完成了三件更基础的工作:
- 调查官方 Tour 和多个现有翻译项目;
- 从固定的官方源码中提取可独立运行的英文基线;
- 为 101 个课程页面建立机器可读目录、持久身份和上游变化预览。
这一阶段还专门确认了右侧 Go 示例代码的运行方式。
A Tour of Go 不只是一组静态课程文章。读者可以直接修改示例代码、格式化代码并点击 Run。如何保留这项能力,同时又不在普通 Web 服务器上运行陌生代码,是这个项目绕不开的问题。
本文不再重复上一篇文章已经说明的完整页面翻译原则,而是记录这些设计进入代码实现以后,最终形成了什么。
一、开始开发以前,我先调查了六个翻译项目
在导入官方源码之前,我先对六个具有代表性的 A Tour of Go 翻译项目进行了只读调查:
Go-zh/websiteGo-zh/touratotto/go-tour-jpgolang-id/tourgolangbr/go-tour-brsuzusime/go-tour-jp
这些项目覆盖了几种常见路线:
| 路线 | 代表形式 |
|---|---|
完整复制 golang/website | 中文 website 项目 |
| 复制旧版单语言 Tour | 中文、日语、印尼语和巴西葡萄牙语项目 |
| 在旧 Tour 上生成静态页面 | 日语静态项目 |
| 每种语言独立部署 | 多数社区翻译站 |
这些项目并不是没有价值。
相反,它们证明了完整 .article 翻译、课程正文与公共 UI 分离、本地运行使用 /socket、正式网站调用远程 Playground 等基本方向是可行的。
但经过多年维护以后,一些共同问题也逐渐显现出来。
二、单语言复制能够快速上线,但长期维护成本很高
多数旧翻译项目采用的方式可以概括为:
复制一套 Tour 服务端
→ 复制模板和静态资源
→ 复制全部 Go 示例
→ 翻译课程正文
→ 单独部署一个语言站
这种方式的优点很明显。
项目刚开始时,维护者只需要复制官方代码、修改 .article 和 UI 文案,就能较快得到一个可以运行的翻译站。
问题在于,每增加一种语言,就会增加一套独立维护的服务端、模板、JavaScript、示例代码和部署配置。
官方 Tour 更新以后,各个翻译项目都需要分别同步:
- 服务端代码;
- 前端资源;
- 课程页面;
- Go 示例;
- 第三方依赖;
- 部署运行时。
如果某个语言项目停止维护,它往往不会立即完全失效。
更常见的情况是:网站暂时还能打开,但课程内容、依赖和部署方式已经逐渐落后。
因此,这次项目没有选择为简体中文复制一套永久独立的 Tour,也没有计划以后为日语、德语或其他语言继续复制更多服务端。
当前确定的原则是:
- Go 服务端尽量共享;
- 模板基础结构和静态资源尽量共享;
- Go 示例只维护一份官方基准;
- 每种语言只独立维护课程译文、UI 文案、术语表和页面状态。
三、完整复制 website 又会产生另一类问题
当前官方 A Tour of Go 已经位于 golang/website 仓库中。
最直接的方案似乎是 Fork 整个 website,然后在里面修改 Tour。
但 golang/website 不只包含 Tour,还包含:
- Go 官方网站服务;
- 博客和文档内容;
- 下载页面;
- 包文档相关逻辑;
- 官方构建和部署配置;
- App Engine 与 Cloud Build 文件;
- 与 Tour 无关的大量静态资源和工具。
如果只想维护一个多语言 Tour,复制整个 website 会让每次上游同步都混入大量无关变化。
因此,我最终采用的是另一种方式:
从固定的官方提交中,提取 Tour 能够独立运行所需要的最小源码闭包。
当前固定的官方基线提交为:
e11dacba76c5aae474746e9eedee19693f492803
首次导入共涉及 199 个上游来源文件:
- 192 个文件保持逐字节一致;
- 7 个文件为了适配独立仓库进行了最小修改。
项目使用 UPSTREAM_MANIFEST.tsv 记录每个来源文件的:
- 上游路径;
- 本地路径;
- 文件类型;
- 是否保持原样;
- SHA-256;
- 必要的适配说明。
这样既避免了复制整个 website,也避免了每次构建时临时下载源码造成的网络依赖和不可重复性。
四、从其他翻译项目中看到的几个长期问题
除了重复维护服务端以外,这次调查还暴露出几个更值得注意的问题。
1. 很多项目没有明确的上游版本
Git 仓库中有一份课程译文,并不能直接说明它对应官方的哪个提交。
最近有提交,也不代表课程正文已经同步。
因此,本项目同时记录:
- 固定 upstream commit;
- 文件级 SHA-256;
- 页面级
source_sha256; - 页面身份和当前路径;
- 未来上游变化的分类结果。
2. 部署平台退役可能导致整个语言站下线
多个旧翻译项目与 App Engine runtime 深度绑定。
当旧 runtime 退役以后,一批社区翻译站曾经同时离线。官方 Go 项目虽然在页面中提供了这些语言入口,但这些站点由社区维护者拥有,官方无法直接替维护者恢复部署。
这说明:
源码仍然存在,不等于线上服务能够长期运行。
因此,当前项目不会把某个云平台的专有部署文件作为核心架构,也不会把个人项目 ID、账号配置或固定运行时写入通用流程。
3. 缺少自动校验时,错误主要依赖读者发现
现有翻译项目通常拥有基础 Go 测试,但很少具备:
- 英文页面与译文结构比较;
.play路径完整性检查;- 链接目标检查;
- 行内代码检查;
- 页面身份检查;
- 翻译状态门禁;
- 上游变化预览;
- 损坏译文阻止发布。
这意味着译文即使破坏了课程结构,也可能要等到维护者或读者偶然发现。
4. 示例代码很容易产生语言分叉
部分翻译项目修改过示例代码中的注释或字符串。
短期看,这样可以让代码显得更本地化;长期看,却会导致不同语言拥有不同的示例基准。
官方修改代码以后,维护者不仅要同步代码,还要判断每个语言版本中的修改是否仍然适用。
因此,第一阶段决定:
.play引用的 Go 示例保持官方原样;- 不翻译变量名、函数名和输出字符串;
- 不为每种语言复制一套不同的示例;
- 使用哈希和测试持续确认示例没有发生意外变化。
5. 纯静态页面更容易部署,但会失去完整交互
静态日语项目证明,课程正文可以预生成成普通 HTML。
这种方式迁移方便,也不依赖持续运行的 Go 服务。
但静态页面通常无法完整保留:
- 编辑器;
- Run;
- Format;
- Kill;
- 与 Playground 的交互。
因此,静态页面未来可以作为只读备份或故障降级产物,但不适合作为当前项目唯一的正式形态。
五、英文 Tour 基线已经可以独立运行
完成最小源码导入以后,项目已经不再依赖完整的 golang/website 仓库才能运行 Tour。
当前英文基线已经完成以下验证:
- Go Module 可以正常解析;
- 所有 Go 测试通过;
- 七个
.article可以被present解析; - 示例代码可以完成原有构建和运行测试;
- Tour HTTP 服务可以正常启动;
- 页面、CSS、JavaScript、Logo 和 favicon 可以访问;
/_/fmt可以正常格式化代码;- 本地
/socket可以完成 WebSocket 握手; - 进程退出后没有监听端口或临时文件残留。
当前源码结构统计为:
| 项目 | 数量 |
|---|---|
顶层 .article | 7 |
| standalone 普通页面 | 101 |
普通 .play 引用 | 92 |
.image 引用 | 1 |
#appengine: 条件页面 | 2 |
这组 7 / 101 / 92 / 1 数据,已经成为当前固定英文基线的一部分。
六、为什么上一篇是 103 页,现在状态文件却只有 101 页
上一篇文章从官方 integrated production 投影视角统计,得到了 103 个页面。
其中多出的两个页面是:
Go offline (optional)The Go Playground
它们来自 welcome.article 中的 #appengine: 条件内容。
真正开始导入独立 Tour 以后,我决定先以当前 standalone 运行方式中的 101 个普通页面建立第一阶段状态,同时将两个条件页面单独记录在:
data/tour-conditional-pages.tsv
因此,现在的结构并不是忽略那两个页面,而是:
101 个 standalone 普通页面
→ 进入 zh-CN 页面状态
2 个 appengine 条件页面
→ 单独登记
→ 暂不进入第一阶段翻译状态
这样可以让第一阶段的页面目录与当前独立运行基线保持一致,同时保留条件页面的来源、标题和哈希。
以后真正实现生产投影或者等价页面时,再明确决定如何处理这两个页面,而不是现在把 standalone 与 App Engine 两种运行模式混在同一个状态文件中。
七、右侧的 Go 示例代码到底在哪里运行
A Tour of Go 最重要的特点之一,是读者可以直接修改右侧代码并点击 Run。
但是,“点击 Run”并不意味着所有环境都以相同方式运行代码。
本地开发模式
本地启动 Tour 时,执行链路大致是:
浏览器中的代码编辑器
→ WebSocket /socket
→ 本地 Tour 进程
→ 本机 Go 工具链
→ 返回运行结果
这种方式适合个人电脑上的本地学习和开发。
当前项目导入英文基线以后,也保留了这项上游行为,并实际验证了 /socket 能够完成 WebSocket 101 Switching Protocols 握手。
但 /socket 会调用 Tour 所在机器上的 Go 工具链执行代码,因此只能用于可信的本地开发环境。
它不能直接作为公开网站的代码执行接口。
官方生产模式
当前官方 Tour 的生产链路并不是浏览器直接在本地运行 Go,也不是让 go.dev 普通 Web 进程直接执行用户代码。
实际链路是:
浏览器
→ 同源 /_/compile
→ go.dev 服务端代理
→ play.golang.org/compile
→ Go Playground 沙箱
→ 返回运行结果
Go Playground 会在受限沙箱中编译和运行代码,并对执行时间、CPU、内存和外部访问进行限制。官方也允许有益于 Go 社区的第三方服务使用 Playground,但要求调用方事先联系并提供可识别的独特 User-Agent。(Go)
当前项目未来采用的方式
正式网站仍然计划保留 Run,但不会把公开 /socket 暴露到互联网。
计划中的正式链路是:
浏览器
→ 项目同源轻量代理
→ Go 官方 Playground
→ 返回运行结果
这里的“代理”只负责转发受限制的请求。
它不代表用户代码在项目 Web 服务器上运行。
这样可以继续保留交互式学习体验,同时避免自己承担恶意代码隔离、系统调用限制和完整沙箱运维。
不过,这部分目前仍然只是已经确定的正式方案,代理尚未开始实现,也没有公开上线。
八、101 个课程页面已经拥有机器可读目录
完成英文基线以后,项目没有直接开始翻译,而是先生成:
data/tour-pages.tsv
其中每个页面都记录:
page_id- 所属 article;
- 当前 Section 编号;
- 当前 route;
- 英文标题;
- 完整页面
source_sha256; .play数量;.image数量。
例如,一个页面可以拥有如下关系:
page_id: welcome/1
article: welcome.article
section_number: 1
route: /welcome/1
source_sha256: 491d9386ffb917083a1e556bc9849d57894dd8b29bd243ac855cd9e99f701893
这里的哈希不是只根据标题生成,也不是根据抽象语法树生成。
它对应的是从英文 article 中导出的完整顶层页面源文本,包括:
- 页面标题;
- 普通正文;
- present 指令;
- 代码引用;
- 结尾换行。
以后官方修改某个页面时,项目可以准确判断英文源是否发生变化,而不是只依赖文件修改时间或人工肉眼比较。
九、页面可以按完整 Section 单独导出
当前维护工具已经可以根据 page_id 导出一个完整英文页面:
go run -mod=readonly ./cmd/tour-i18n page export \
--id welcome/1 \
--output /tmp/welcome-1.article
导出结果:
- 只包含一个完整顶层 Section;
- 不包含同一个 article 中的其他页面;
- 保持原始 present 格式;
- 可以重新解析;
- SHA-256 与页面目录一致;
- 默认不会覆盖已有文件。
未来 GLM-5.2 接收的就是这样的完整页面,而不是从页面中拆出的若干句子或 JSON 字段。
这项设计原则已经在上一篇文章中详细说明,本文不再重复展开。
十、zh-CN 的 101 个页面目前全部是 pending
项目已经创建独立的简体中文语言空间:
locales/zh-CN/
其中分别维护:
- locale 元数据;
- 页面状态;
- 未来的完整页面译文;
- UI 文案边界;
- 术语表。
当前 status.tsv 中恰好有 101 条记录:
pending:101
candidate:0
ready:0
blocked:0
published:0
这意味着项目已经知道需要处理哪些页面,但还没有生成任何正式中文译文。
没有英文内容冒充中文页面,也没有为了展示进度提前创建空的 .article 文件。
十一、结构校验不再锁死普通段落
上一篇设计阶段曾经倾向于严格保持普通段落的数量、顺序和位置。
真正分析 101 个页面并实现 candidate 校验器以后,我调整了这项规则。
现在的原则变为:
只严格保护真正危险或不可翻译的内容,不因为英文段落形式限制正常的中文表达。
当前必须保持的内容包括:
.play指令及目标路径;.image指令及目标路径;- 链接目标 URL;
- JavaScript 链接目标;
- 行内代码;
- 预格式化代码;
- present 指令;
- protected 内容的数量和顺序;
- 顶层 Section 数量。
允许变化的内容包括:
- 页面标题;
- 普通正文;
- 中文语序;
- 句子数量;
- 普通段落数量;
- 普通文本换行;
- 链接显示文字。
例如,下面的链接显示文字可以翻译:
[[/cmd/gofmt/][gofmt tool]]
但是链接目标 /cmd/gofmt/ 不能被模型修改。
同样,.play welcome/hello.go 可以出现在译文页面中,但其中的路径必须保持原样。
这样既保留整页翻译所需要的上下文,也避免为了追求机械结构一致而产生生硬中文。
十二、page_id 不能永远等于页面位置
最初生成页面目录时,page_id 看起来与当前路由完全一致:
page_id: basics/5
route: /basics/5
但如果官方以后在 Basics 的第 5 页前面插入一页,原来的第 5 页就会移动到第 6 页。
如果继续根据位置重新生成 ID,后续所有页面都会发生变化:
basics/5 → basics/6
basics/6 → basics/7
basics/7 → basics/8
这会进一步导致:
- 译文文件路径变化;
- 状态记录失去关联;
- 历史 Candidate 无法匹配;
- 已发布页面身份漂移;
- 同步结果产生大量无意义变化。
因此,当前 101 个 page_id 已经正式冻结为持久身份。
各字段现在具有不同职责:
| 字段 | 含义 |
|---|---|
page_id | 项目内部持久身份 |
route | 当前访问路径 |
section_number | 当前上游位置 |
source_title | 识别和诊断信息 |
source_sha256 | 当前英文源版本 |
未来即使页面移动,route 和 section_number 可以变化,page_id 仍然保持不变。
新增页面也不会插入编号并重排旧页面,而是显式分配一个从未使用的新 ID。
十三、上游变化现在可以先预览
项目新增了只读的上游变化预览命令:
go run -mod=readonly ./cmd/tour-i18n upstream preview \
--source-root /path/to/website
预览结果将页面分为六种类型:
| 类型 | 含义 |
|---|---|
unchanged | 页面内容和位置都没有变化 |
content_changed | 可以确认是同一页面,但英文内容已更新 |
moved | 完整源内容相同,但位置或 route 发生变化 |
added | 上游新增页面 |
removed | 上游删除页面 |
ambiguous | 证据不足,不能安全自动匹配 |
匹配策略保持保守。
完整源哈希唯一相同,可以确认页面发生了移动。
当前 route 仍然存在,且 protected structure 保持兼容,可以确认普通内容发生了变化。
但以下情况不会自动迁移:
- 页面移动的同时又发生较大结构变化;
.play路径发生变化;- 多个页面具有重复特征;
- 只能依靠标题相同判断;
- 只能依靠普通正文相似判断;
- 跨 article 移动但没有唯一证据。
这些情况会进入 ambiguous,等待明确处理。
预览命令不会:
- 修改正式目录;
- 修改
status.tsv; - 删除语言状态;
- 创建新页面 ID;
- 移动 Candidate;
- 自动覆盖译文。
十四、当前固定基线的预览结果
使用当前固定 upstream 运行两次预览,输出完全一致:
普通页面:101
条件页面:2
unchanged:101
content_changed:0
moved:0
added:0
removed:0
ambiguous:0
conditional unchanged:2
这说明正式页面目录与当前英文源码完全一致,也证明预览输出具有确定性。
与此同时:
- 101 个
page_id没有变化; status.tsv保持逐字节不变;- 两个条件页面目录没有变化;
- 英文 article 没有变化;
- Go 示例没有变化。
十五、多语言域名也从路径方案调整为子域名
设计阶段曾经考虑在同一个域名下使用:
/ja/
/de/
真正考虑 CDN 和部署以后,我改为按语言使用子域名。
当前规划是:
go-tour.<实际域名>
→ 默认简体中文
→ EdgeOne
en.go-tour.<实际域名>
→ 英文
→ Cloudflare
ja.go-tour.<实际域名>
→ 未来日语
→ Cloudflare
默认站点直接提供 zh-CN,不再创建 zh. 或 zh-cn. 子域。
项目介绍、语言选择、上游基线和 GitHub 入口则放在默认站点的 /about/ 页面。
这样不同语言以后可以独立选择:
- CDN;
- 部署区域;
- 缓存策略;
- 发布时间;
- 故障处理方式。
目前这些都只是已经记录的域名和部署规划,尚未创建真实 DNS、证书或 CDN 配置。
十六、这一阶段仍然没有翻译任何正式页面
截至目前,项目已经完成:
- 最小英文 Tour 源码闭包导入;
- 199 个来源文件追踪;
- 英文 Tour 独立运行;
- 七个 article 全量解析;
- 101 个普通页面目录;
- 两个条件页面目录;
- 92 个
.play引用验证; - 示例代码构建和运行测试;
- 单页完整导出;
- zh-CN 状态文件;
- candidate 结构校验;
- 101 个持久
page_id; - 上游变化预览;
- 多语言域名和 CDN 边界规划。
但当前仍然是:
正式中文页面:0
Candidate:0
Ready:0
Blocked:0
Published:0
这不是项目进度停滞。
恰恰相反,这意味着在第一篇译文出现以前,项目已经先建立了:
- 可重复的英文源;
- 可追溯的页面身份;
- 可验证的结构边界;
- 可预览的上游变化;
- 不依赖人工记忆的状态基础。
十七、下一步才开始真正调用 GLM-5.2
下一阶段将进入正式翻译流水线:
根据 page_id 导出完整英文页面
→ 保护危险结构
→ 调用 GLM-5.2 整页翻译
→ 保存 Candidate
→ present 重新解析
→ protected structure 比较
→ 更新尝试次数和页面状态
这一阶段仍然不会立即批量处理全部 101 页。
更稳妥的方式是:
- 先实现单页翻译执行器;
- 使用模拟服务完成自动测试;
- 选择一个较短页面进行第一次真实翻译;
- 验证 Candidate 落盘和失败恢复;
- 验证
ready与blocked状态迁移; - 最后再决定何时开始批量翻译。
到那时,这个项目才会真正生成第一篇简体中文课程页面。
而现在完成的工作,是确保第一篇译文出现以后,它不会成为一份无法确认来源、无法判断是否过期、也无法自动验证结构的孤立文本。
需要长期技术维护或远程问题排查?
我是拥有 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

发表回复