经过前面一系列翻译、结构保护、自动校验、公共 UI 本地化以及发布前全局验收,A Tour of Go 多语言翻译项目终于从“内容已经准备完成”走到了真正的生产部署阶段。
这一次上线的简体中文 zh-CN 版本已经完成:
- 103/103 个课程页面;
- 7/7 篇 article metadata;
- 公共 UI 中文化;
- production publish;
- 生产环境静态发布包;
- systemd 服务;
- Nginx 反向代理;
- EdgeOne CDN;
- HTTPS;
- 远程运行与格式化;
- 浏览器最终验收。
正式站点:
https://go-tour.shuijingwanwq.com/
这篇文章记录的重点,不再是翻译本身,而是项目第一次真正进入生产环境时遇到的一系列问题。
其中有几个问题在本地开发环境中几乎不可能提前完全暴露出来,包括:
- glibc 版本不兼容;
- OneinStack 自动生成的 Nginx 静态资源规则抢占反向代理;
- 阿里云 ECS 无法访问
play.golang.org; go.devPlayground 接口请求格式与原代理实现不同;- release 上传后目录权限导致 systemd
203/EXEC; - 已运行的 Nginx 与 systemd PID 状态脱节。
最终这些问题都逐一解决,生产站点正式上线。
一、从 103/103 ready 到真正的 production publish
在正式部署之前,zh-CN 已经完成了全部 103 个课程页面。
最终状态是:
Section: 103/103 ready
article metadata: 7/7
而且此前已经完成:
- present 解析;
- 结构比较;
- 页面渲染;
- HTTP 验收;
- 浏览器人工抽查;
- 公共 UI 本地化。
因此生产部署阶段不再重新做翻译,而是开始解决一个新的问题:
如何把已经通过自动校验的中文内容,以一个可重复、可验证、可回滚的 production release 形式真正部署出去。
最终项目生成的生产发布包包含:
bin/tour
_content/
release.json
SHA256SUMS
并记录:
- locale;
- 页面数量;
- article 数量;
- upstream commit;
- 执行方式;
- Go OS / Arch;
- 是否启用本地
/socket。
生产环境明确关闭本地代码执行接口:
local_socket_enabled = false
浏览器中的运行与格式化功能,则通过服务器代理到远程 Go Playground。
二、第一个生产问题:GLIBC_2.34 不兼容
最初生成的 bin/tour 在本地没有问题,但上传到服务器后无法运行。
进一步检查发现,生产服务器上的 glibc 为:
glibc 2.32
而本地构建出来的程序依赖:
GLIBC_2.34
也就是说,这个二进制文件实际上携带了比服务器更高的 glibc 运行时要求。
对于这种 Go 服务,最直接的解决办法不是升级整台服务器的 glibc,而是让生产二进制彻底摆脱这类动态依赖。
最终 production build 显式使用:
CGO_ENABLED=0
重新生成发布包后:
ELF 64-bit LSB executable
statically linked
继续检查:
ldd bin/tour
得到:
not a dynamic executable
这样生产二进制就不再依赖服务器上的 glibc 版本。
这也是后来 production publish 固定的一项构建约束。
三、正式部署目录与 systemd 服务
为了不继续占用系统盘,我最终将 Go Tour 部署在:
/data/go-tour/
采用 release + current 软链接的结构:
/data/go-tour/
├── current -> releases/20260811-zh-CN-925d59d
└── releases/
├── 20260811-zh-CN-0fd1001
└── 20260811-zh-CN-925d59d
这种方式的一个好处是,升级版本时不直接覆盖正在使用的文件。
新版本上传并完成校验后,只需要切换:
/data/go-tour/current
旧 release 仍然可以暂时保留,用作回滚。
Go Tour 本身由独立的 systemd 服务运行:
go-tour.service
只监听本机:
127.0.0.1:3999
外部用户无法直接访问这个端口。
完整链路最终是:
浏览器
→ EdgeOne
→ Nginx
→ Go Tour
→ go.dev Playground
四、页面打开了,但 CSS 和 JavaScript 全部 404
第一次完成 Nginx 和 EdgeOne 接入后,浏览器已经可以访问中文课程页面。
但页面明显不正常:
- HTML 有内容;
- 中文正文也正常;
- CSS 没有加载;
- JavaScript 没有加载;
- 整个页面处于近似“裸 HTML”的状态。
先绕过 Nginx,直接请求 Go Tour:
127.0.0.1:3999
CSS、JavaScript 都返回 200。
但是通过 Nginx 请求相同资源时却返回 404。
因此问题已经可以限定为:
Go Tour 本身没有问题,错误发生在 Nginx。
检查 OneinStack 自动生成的站点配置后发现,它除了:
location / {
proxy_pass http://127.0.0.1:3999;
}
之外,还自动生成了针对静态文件的正则 location。
例如 CSS、JS、PNG 等资源会优先命中这些规则,而不是进入 location / 的反向代理。
对于传统 PHP 网站,这类规则很常见。
但 Go Tour 的 /tour/static/... 实际是由 Go 服务自己提供的,因此这些静态文件规则反而把正常请求截走了。
最终删除该站点中两组不适合反向代理应用的静态资源 location,保留:
location / {
proxy_pass http://127.0.0.1:3999;
}
再次 reload Nginx 后:
/tour/static/css/app.css → 200
/tour/static/... → 200
/tour/script.js → 200
浏览器页面恢复正常。
五、页面正常了,“运行”按钮却报错
静态资源问题解决后,中文页面已经完整显示。
但点击右侧的“运行”按钮时,却出现:
Error communicating with remote server.
程序已退出。
这时首先要区分,到底是哪一层出问题:
浏览器
→ EdgeOne
→ Nginx
→ Go Tour
→ Playground
检查前端 script.js 后确认,“运行”实际请求:
POST /_/compile?backend=
格式化对应:
POST /_/fmt?backend=
直接请求本机 Go Tour:
http://127.0.0.1:3999/_/compile?backend=
得到:
HTTP 502
Playground compile unavailable
这一步非常关键。
因为请求已经绕过 EdgeOne 和 Nginx,所以问题已经和 CDN、反向代理无关。
真正失败的是:
Go Tour → 远程 Playground。
六、生产服务器无法连接 play.golang.org
当时 production proxy 使用的是:
https://play.golang.org
直接在阿里云 ECS 上测试:
https://play.golang.org/
结果:
HTTP 000
Connection timed out
但是测试:
https://go.dev/play/
却可以正常得到:
HTTP 200
进一步直接向:
https://go.dev/_/compile
发送真实代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
能够成功返回:
Hello, 世界

play.golang.org 上游,而 go.dev/_/compile 可以正常完成远程代码执行。这时生产环境“运行”失败的根因已经非常明确:
play.golang.org:443
→ 阿里云 ECS 连接超时
go.dev
→ 可以正常访问
七、不只是换域名:go.dev 的接口路径和请求格式也不同
一开始看起来,解决方法似乎只是:
play.golang.org
→ go.dev
但真正修改 production proxy 时,又发现还有两个细节。
1. 接口路径不同
新的实际接口是:
https://go.dev/_/compile
https://go.dev/_/fmt
因此最终 production 上游基础地址使用:
https://go.dev/_
再由代理拼出:
/compile
/fmt
2. Compile 请求必须保持 form 编码
这是这次修复中更容易忽略的一点。
浏览器传给 Go Tour 的字段包括:
version
body
withVet
原代理中存在一次请求转换。
但实际验证发现:
go.dev/_/compile需要原来的 form 请求格式。
如果把请求重新编码成不兼容的形式,即使远端 HTTP 层面能够响应,也拿不到正常的执行事件。
因此最终修改并不是简单替换一个 hostname,而是:
- production provider 改为
go.dev; - compile 使用
https://go.dev/_/compile; - fmt 使用
https://go.dev/_/fmt; - compile 保留
version、body、withVet的 form 编码; - 浏览器接口保持原样;
/socket仍然关闭。
重新测试后:
/_/compile → 200
/_/fmt → 200
/socket → 404
/_/share → 404
生产 release 的 provider 也正式更新为:
execution_provider = go.dev
八、新 release 第一次切换失败:systemd 203/EXEC
完成 go.dev 修复后,新 release 上传到了:
/data/go-tour/releases/20260811-zh-CN-925d59d
然后把:
/data/go-tour/current
切换到新版。
但 systemd 启动后,HTTP 健康检查失败。
进一步查看日志发现:
go-tour.service: Main process exited, code=exited, status=203/EXEC
203/EXEC 的含义非常明确:
systemd 连
ExecStart指定的程序都没能成功执行。
最开始还怀疑是不是新二进制本身有问题。
但比较新旧 release 权限后,马上发现了真正原因:
旧版本:
drwxr-xr-x root root
新版本:
drwx------ www www
虽然:
bin/tour
本身有执行权限,但 go-tour 服务用户无法穿过上层 700 目录,因此根本访问不到二进制文件。
使用:
runuser -u go-tour -- test -x ...
验证后也明确显示:
new: NOT executable by go-tour
old: executable by go-tour
最终将新 release 规范为:
root:root
755
再次验证:
new: executable by go-tour
之后重新切换:
current=/data/go-tour/releases/20260811-zh-CN-925d59d
HTTP=200
systemd 正常启动。
九、最终生产 release
最终正式使用的 production release 为:
20260811-zh-CN-925d59d
对应项目 commit:
925d59d92016e026c92ae60f4535abd9237119ea
生产状态:
locale : zh-CN
pages : 103
articles : 7
execution_provider : go.dev
local_socket : False
二进制:
ELF 64-bit LSB executable
x86-64
statically linked

go.dev,本地 /socket 关闭,生产二进制采用静态链接。十、Nginx 明明运行,systemd 却显示 failed
部署过程中还遇到了另一个比较独立的问题。
当时 Nginx 明明正常提供:
80
443
但:
nginx.service
却显示:
ActiveState=failed
MainPID=0
实际运行的 master 则是:
375418
而:
/var/run/nginx.pid
已经变成了 0 字节文件。
检查配置:
systemd PIDFile=/var/run/nginx.pid
nginx.conf pid=/var/run/nginx.pid
两边路径其实完全一致。
真正的问题是:
当前 Nginx master 并不是 systemd 当前这一轮启动出来的,而 systemd 后来又尝试启动了第二个 Nginx。
因为旧 master 已经占用 80/443,新的 Nginx 自然启动失败。
最终没有直接:
systemctl restart nginx
更没有强杀旧进程。
而是先验证:
nginx configuration file ... test is successful
然后:
- 找到唯一的旧 Nginx master;
- 使用
QUIT优雅退出; - 确认旧 master 已经退出;
- 清理无效 PID 文件;
systemctl reset-failed nginx.service;- 重新由 systemd 启动 Nginx。
最终:
Active: active (running)
Main PID: 611635
并且:
/var/run/nginx.pid
→ 611635
至此 Nginx 正式重新回到 systemd 管理。
以后就可以正常使用:
systemctl reload nginx
或者:
systemctl restart nginx
十一、浏览器最终验收
所有服务器端检查完成后,最后还是要回到真正的浏览器。
打开:
https://go-tour.shuijingwanwq.com/tour/welcome/1
中文课程正文、代码编辑器以及页面样式全部正常。
点击“运行”后,右下方正常输出:
Hello, 世界
程序已退出。
“格式化”按钮同样可以正常工作。

这一步其实非常重要。
因为此前我们分别验证过:
- Go Tour 后端;
- Nginx;
- EdgeOne;
- Playground API。
而浏览器真实点击“运行”和“格式化”,才是从最终用户角度完成整个链路的验收。
十二、最终公网冒烟验收
Nginx 完成 systemd 接管之后,我又做了一轮最终公网检查。
结果为:
课程页面 → HTTP 200
/_/compile → HTTP 200
运行输出 → Hello, 世界
/_/fmt → HTTP 200
/socket → HTTP 404
当前 release:
20260811-zh-CN-925d59d

/socket 保持 404,正式生产版本为 925d59d。这里还有一个很有意思的小插曲。
第一次整理这张博客截图时,/_/compile 曾经出现一次瞬时 502。
为了避免把一个偶发问题直接当成结论,又分别进行了三轮测试:
Go Tour 本机直连 → 5/5 HTTP 200
Nginx 源站 HTTPS → 5/5 HTTP 200
EdgeOne 公网 → 5/5 HTTP 200
没有再次复现。
因此目前没有证据表明生产运行链路存在持续故障。
这也提醒我:
部署验收不能只看一次成功,也不能因为一次偶发失败就立即修改架构。先把链路逐层隔离,再看问题是否能够稳定复现,往往比直接“修”更重要。
十三、为什么最终仍然保留 /tour
正式上线后,我又注意到一个小问题。
当前 URL 是:
https://go-tour.shuijingwanwq.com/tour/list
https://go-tour.shuijingwanwq.com/tour/welcome/1
由于子域名本身已经叫:
go-tour
再加一层:
/tour/
第一眼看起来确实有些重复。
因此又专门分析了一次,是否应该改成:
https://go-tour.shuijingwanwq.com/list
https://go-tour.shuijingwanwq.com/welcome/1
最后还是决定:
保留
/tour。
原因是 /tour 并不是这次生产部署额外添加的一层路径,而是官方 A Tour of Go 自身的核心路由结构。
它涉及:
- Go HTTP 路由;
- Angular 路由;
- 课程导航;
- TOC;
- HTML 模板;
- partial;
- 课程内部链接;
- 静态资源;
- lesson API;
- preview;
- webtest;
- 大量既有测试。
如果强行去掉 /tour,预计需要维护 10~15 个代码、模板和测试文件。
如果再彻底清理 103 个课程页面中的既有 /tour/... 链接,还会进一步触及:
- 英文源;
- zh-CN candidate;
- source hash;
- status;
- catalog。
而收益主要只是:
URL 看起来更简洁一点。
对于一个长期需要持续同步 upstream 的项目来说,这笔维护成本并不划算。
所以最终仍然保持官方结构。
十四、正式上线后的最终架构
现在的生产访问链路最终固定为:
Cloudflare 权威 DNS
↓
Tencent EdgeOne
↓
Nginx
↓
Go Tour
127.0.0.1:3999
↓
go.dev Playground
Cloudflare 这里只负责权威 DNS。
业务 CNAME 使用:
DNS only
不再额外套一层 Cloudflare Proxy,因此不存在 Cloudflare + EdgeOne 双层 CDN。
代码执行方面:
/socket
仍然没有暴露:
HTTP 404
Run 和 Format 则通过:
/_/compile
/_/fmt
由服务器代理至:
go.dev
这也是当前 production 架构和本地开发模式之间最重要的安全边界之一。
十五、这次上线最值得记录的几个教训
到这一步,A Tour of Go 多语言翻译项目终于完成了 zh-CN 第一阶段真正意义上的上线。
回头看,这次生产部署中最值得记录的并不是某一条命令,而是几个更普遍的经验。
1. 本地通过,不等于生产二进制一定能运行
Go 项目如果没有明确控制 CGO,仍然可能把系统运行时依赖带进生产包。
生产发布最好主动验证:
file
ldd
而不是等服务器报错之后才发现 glibc 不兼容。
2. 通用 Nginx 模板并不一定适合反向代理应用
OneinStack 为普通网站生成的 CSS、JS 静态资源缓存规则本身没有错。
错的是:
把一套针对传统网站的默认假设,直接套到了由 Go 自己提供静态资源的应用上。
3. “域名换掉”不等于 API 就兼容
从:
play.golang.org
迁移到:
go.dev
最终不仅修改了域名,还涉及:
- 路径;
- form 编码;
- compile/fmt 请求行为;
- production manifest。
如果当时只改 hostname,很可能会得到一个“HTTP 200,但实际运行行为不正确”的半成品。
4. systemd 的 203/EXEC 不一定是二进制损坏
这次真正的问题只是:
release 根目录 700
导致服务用户无法穿越目录。
所以遇到 203/EXEC 时,除了检查二进制本身,还要检查从根目录到可执行文件整条路径上的访问权限。
5. 一次偶发失败不能直接推导出架构故障
最终截图阶段出现了一次 502。
如果当时直接开始改 EdgeOne、Nginx 或 Go Tour,很可能又引入新的问题。
后来逐层连续测试:
5/5
5/5
5/5
才确认没有持续性故障。
这种“先复现,再修复”的原则,在生产环境尤其重要。
十六、一个阶段真正结束了
从项目刚开始时决定:
以完整课程页面作为最小翻译单元。
到后来一步一步解决:
- Section 结构保护;
- 行内代码换位;
- 链接换位;
- emphasis;
- 静态代码块;
- 教学注释;
#appengine:特殊页面;- 公共 UI;
- article metadata;
- 103 页全量验收;
- production publish;
- 静态二进制;
- Nginx;
- EdgeOne;
- HTTPS;
- Playground;
- systemd;
直到今天真正打开:
https://go-tour.shuijingwanwq.com/
并且可以在浏览器里点击:
运行
格式化
我才认为:
A Tour of Go 多语言翻译项目的 zh-CN 第一阶段,真正完成了。
它已经不再只是一个:
103/103 ready
的本地翻译项目。
而是一个真正可以从公网访问、可以交互执行代码、可以继续跟踪 upstream,并且为未来其他语言保留扩展能力的正式站点。
接下来项目的重点,也终于可以从:
“怎样把第一种语言做出来”
转向:
“怎样长期维护它,以及下一种语言应该从哪里开始”。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 在线学习:A Tour of Go 简体中文版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前第一阶段已完成简体中文版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。


发表回复