没有不值得去解决的问题,也没有不值得去学习的技术!

A Tour of Go 中文版开发实战:英文基线、在线运行与 101 页上游同步体系

作者:

,

上一篇文章中,我完成了 A Tour of Go 中文版项目的设计冻结,重点讨论了页面数量、翻译单元、状态流程和命令行架构等问题。(Shuijing Wanwq)

设计冻结以后,我并没有立即调用 GLM-5.2 开始翻译。

真正进入实现阶段后,我首先完成了三件更基础的工作:

  1. 调查官方 Tour 和多个现有翻译项目;
  2. 从固定的官方源码中提取可独立运行的英文基线;
  3. 为 101 个课程页面建立机器可读目录、持久身份和上游变化预览。

这一阶段还专门确认了右侧 Go 示例代码的运行方式。

A Tour of Go 不只是一组静态课程文章。读者可以直接修改示例代码、格式化代码并点击 Run。如何保留这项能力,同时又不在普通 Web 服务器上运行陌生代码,是这个项目绕不开的问题。

本文不再重复上一篇文章已经说明的完整页面翻译原则,而是记录这些设计进入代码实现以后,最终形成了什么。

一、开始开发以前,我先调查了六个翻译项目

在导入官方源码之前,我先对六个具有代表性的 A Tour of Go 翻译项目进行了只读调查:

  • Go-zh/website
  • Go-zh/tour
  • atotto/go-tour-jp
  • golang-id/tour
  • golangbr/go-tour-br
  • suzusime/go-tour-jp

这些项目覆盖了几种常见路线:

路线代表形式
完整复制 golang/website中文 website 项目
复制旧版单语言 Tour中文、日语、印尼语和巴西葡萄牙语项目
在旧 Tour 上生成静态页面日语静态项目
每种语言独立部署多数社区翻译站

这些项目并不是没有价值。

相反,它们证明了完整 .article 翻译、课程正文与公共 UI 分离、本地运行使用 /socket、正式网站调用远程 Playground 等基本方向是可行的。

但经过多年维护以后,一些共同问题也逐渐显现出来。

二、单语言复制能够快速上线,但长期维护成本很高

多数旧翻译项目采用的方式可以概括为:

Plaintext
复制一套 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 握手;
  • 进程退出后没有监听端口或临时文件残留。

当前源码结构统计为:

项目数量
顶层 .article7
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

因此,现在的结构并不是忽略那两个页面,而是:

Plaintext
101 个 standalone 普通页面
→ 进入 zh-CN 页面状态

2 个 appengine 条件页面
→ 单独登记
→ 暂不进入第一阶段翻译状态

这样可以让第一阶段的页面目录与当前独立运行基线保持一致,同时保留条件页面的来源、标题和哈希。

以后真正实现生产投影或者等价页面时,再明确决定如何处理这两个页面,而不是现在把 standalone 与 App Engine 两种运行模式混在同一个状态文件中。

七、右侧的 Go 示例代码到底在哪里运行

A Tour of Go 最重要的特点之一,是读者可以直接修改右侧代码并点击 Run。

但是,“点击 Run”并不意味着所有环境都以相同方式运行代码。

本地开发模式

本地启动 Tour 时,执行链路大致是:

Plaintext
浏览器中的代码编辑器
→ WebSocket /socket
→ 本地 Tour 进程
→ 本机 Go 工具链
→ 返回运行结果

这种方式适合个人电脑上的本地学习和开发。

当前项目导入英文基线以后,也保留了这项上游行为,并实际验证了 /socket 能够完成 WebSocket 101 Switching Protocols 握手。

/socket 会调用 Tour 所在机器上的 Go 工具链执行代码,因此只能用于可信的本地开发环境。

它不能直接作为公开网站的代码执行接口。

官方生产模式

当前官方 Tour 的生产链路并不是浏览器直接在本地运行 Go,也不是让 go.dev 普通 Web 进程直接执行用户代码。

实际链路是:

Plaintext
浏览器
→ 同源 /_/compile
→ go.dev 服务端代理
→ play.golang.org/compile
→ Go Playground 沙箱
→ 返回运行结果

Go Playground 会在受限沙箱中编译和运行代码,并对执行时间、CPU、内存和外部访问进行限制。官方也允许有益于 Go 社区的第三方服务使用 Playground,但要求调用方事先联系并提供可识别的独特 User-Agent。(Go)

当前项目未来采用的方式

正式网站仍然计划保留 Run,但不会把公开 /socket 暴露到互联网。

计划中的正式链路是:

Plaintext
浏览器
→ 项目同源轻量代理
→ Go 官方 Playground
→ 返回运行结果

这里的“代理”只负责转发受限制的请求。

它不代表用户代码在项目 Web 服务器上运行。

这样可以继续保留交互式学习体验,同时避免自己承担恶意代码隔离、系统调用限制和完整沙箱运维。

不过,这部分目前仍然只是已经确定的正式方案,代理尚未开始实现,也没有公开上线。

八、101 个课程页面已经拥有机器可读目录

完成英文基线以后,项目没有直接开始翻译,而是先生成:

data/tour-pages.tsv

其中每个页面都记录:

  • page_id
  • 所属 article;
  • 当前 Section 编号;
  • 当前 route;
  • 英文标题;
  • 完整页面 source_sha256
  • .play 数量;
  • .image 数量。

例如,一个页面可以拥有如下关系:

Plaintext
page_id: welcome/1
article: welcome.article
section_number: 1
route: /welcome/1
source_sha256: 491d9386ffb917083a1e556bc9849d57894dd8b29bd243ac855cd9e99f701893

这里的哈希不是只根据标题生成,也不是根据抽象语法树生成。

它对应的是从英文 article 中导出的完整顶层页面源文本,包括:

  • 页面标题;
  • 普通正文;
  • present 指令;
  • 代码引用;
  • 结尾换行。

以后官方修改某个页面时,项目可以准确判断英文源是否发生变化,而不是只依赖文件修改时间或人工肉眼比较。

九、页面可以按完整 Section 单独导出

当前维护工具已经可以根据 page_id 导出一个完整英文页面:

Bash
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 条记录:

Plaintext
pending:101
candidate:0
ready:0
blocked:0
published:0

这意味着项目已经知道需要处理哪些页面,但还没有生成任何正式中文译文。

没有英文内容冒充中文页面,也没有为了展示进度提前创建空的 .article 文件。

十一、结构校验不再锁死普通段落

上一篇设计阶段曾经倾向于严格保持普通段落的数量、顺序和位置。

真正分析 101 个页面并实现 candidate 校验器以后,我调整了这项规则。

现在的原则变为:

只严格保护真正危险或不可翻译的内容,不因为英文段落形式限制正常的中文表达。

当前必须保持的内容包括:

  • .play 指令及目标路径;
  • .image 指令及目标路径;
  • 链接目标 URL;
  • JavaScript 链接目标;
  • 行内代码;
  • 预格式化代码;
  • present 指令;
  • protected 内容的数量和顺序;
  • 顶层 Section 数量。

允许变化的内容包括:

  • 页面标题;
  • 普通正文;
  • 中文语序;
  • 句子数量;
  • 普通段落数量;
  • 普通文本换行;
  • 链接显示文字。

例如,下面的链接显示文字可以翻译:

Plaintext
[[/cmd/gofmt/][gofmt tool]]

但是链接目标 /cmd/gofmt/ 不能被模型修改。

同样,.play welcome/hello.go 可以出现在译文页面中,但其中的路径必须保持原样。

这样既保留整页翻译所需要的上下文,也避免为了追求机械结构一致而产生生硬中文。

十二、page_id 不能永远等于页面位置

最初生成页面目录时,page_id 看起来与当前路由完全一致:

Plaintext
page_id: basics/5
route: /basics/5

但如果官方以后在 Basics 的第 5 页前面插入一页,原来的第 5 页就会移动到第 6 页。

如果继续根据位置重新生成 ID,后续所有页面都会发生变化:

Plaintext
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当前英文源版本

未来即使页面移动,routesection_number 可以变化,page_id 仍然保持不变。

新增页面也不会插入编号并重排旧页面,而是显式分配一个从未使用的新 ID。

十三、上游变化现在可以先预览

项目新增了只读的上游变化预览命令:

Bash
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 运行两次预览,输出完全一致:

Plaintext
普通页面: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 示例没有变化。

十五、多语言域名也从路径方案调整为子域名

设计阶段曾经考虑在同一个域名下使用:

Plaintext
/ja/
/de/

真正考虑 CDN 和部署以后,我改为按语言使用子域名。

当前规划是:

Plaintext
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 边界规划。

但当前仍然是:

Plaintext
正式中文页面:0
Candidate:0
Ready:0
Blocked:0
Published:0

这不是项目进度停滞。

恰恰相反,这意味着在第一篇译文出现以前,项目已经先建立了:

  • 可重复的英文源;
  • 可追溯的页面身份;
  • 可验证的结构边界;
  • 可预览的上游变化;
  • 不依赖人工记忆的状态基础。

十七、下一步才开始真正调用 GLM-5.2

下一阶段将进入正式翻译流水线:

Plaintext
根据 page_id 导出完整英文页面
→ 保护危险结构
→ 调用 GLM-5.2 整页翻译
→ 保存 Candidate
→ present 重新解析
→ protected structure 比较
→ 更新尝试次数和页面状态

这一阶段仍然不会立即批量处理全部 101 页。

更稳妥的方式是:

  1. 先实现单页翻译执行器;
  2. 使用模拟服务完成自动测试;
  3. 选择一个较短页面进行第一次真实翻译;
  4. 验证 Candidate 落盘和失败恢复;
  5. 验证 readyblocked 状态迁移;
  6. 最后再决定何时开始批量翻译。

到那时,这个项目才会真正生成第一篇简体中文课程页面。

而现在完成的工作,是确保第一篇译文出现以后,它不会成为一份无法确认来源、无法判断是否过期、也无法自动验证结构的孤立文本。

需要长期技术维护或远程问题排查?

我是拥有 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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理