系列帖子
上一篇完成了 ZgoCloud Playground 执行节点的搭建。
新的执行接口已经准备好:
https://play.go-dev.shuijingwanwq.com:8443/compile
https://play.go-dev.shuijingwanwq.com:8443/fmt
它们分别固定转发到 Go 官方 Playground:
https://play.golang.org/compile
https://play.golang.org/fmt
但做到这一步,还只是解决了“新的执行节点已经可以工作”。
生产环境中的 A Tour of Go 浏览器代码仍然使用原来的同源接口:
/_/compile
/_/fmt
也就是说,真实链路仍然是:
浏览器
→ EdgeOne
→ 阿里云
→ Go Tour
→ go.dev Playground
这篇继续记录第二阶段:
如何让生产环境中的 Run 和 Format 真正切换到新的 ZgoCloud 执行节点,以及上线后为什么明明新版本已经部署成功,浏览器却一度仍然执行旧逻辑。
一、先确认生产环境到底是怎么执行代码的
在正式修改之前,我先让 Codex 对 go-tour-i18n 做了一次只读审计。
这一轮没有急着改代码,而是先把 Run、Format、本地 preview 和 production 的请求路径全部找清楚。
结果比最初想象的稍微复杂一些。
生产环境中的 Run 使用:
HTTPTransport
最终由 _content/js/playground.js 发起:
POST /_/compile?backend=...
但是 Format 并不完全走同一个 HTTPTransport。
仓库里实际上存在两处 Format 请求:
_content/js/playground.js
_content/tour/static/js/services.js
它们最终都会请求:
/_/fmt
因此,如果只修改 Run,很容易出现:
Run → 新执行节点
Format → 仍然请求旧接口
这种半迁移状态。
二、本地 preview 与 production 不能一起改
审计还确认了一个很重要的区别。
本地 preview 使用:
SocketTransport
通过:
/socket
连接本地运行环境。
而 production 使用:
HTTPTransport
并且生产环境已经明确禁用:
/socket
/socket/
这两个地址在 production 中都会返回:
404
所以这次修改不能简单地把所有:
/_/compile
/_/fmt
全局替换成外部 URL。
我希望最终达到的是:
生产环境:
Run → ZgoCloud /compile
Format → ZgoCloud /fmt
本地 preview:
继续使用 SocketTransport
同时保留原有的相对路径行为,为兼容和回滚留下空间。
三、增加统一的 Playground 基础地址
最终采用的方案是在页面初始化时增加一个统一配置:
window.playgroundBaseURL
生产环境注入:
https://play.go-dev.shuijingwanwq.com:8443
本地 preview 则保持:
""
也就是空字符串。
随后新增一个很小的 URL 处理函数。
逻辑大致是:
如果 playgroundBaseURL 为空:
保持原始路径
如果配置了 playgroundBaseURL:
使用外部基础地址
这样原来的调用仍然可以写成:
/_/compile
/_/fmt
但在生产环境中最终会生成:
https://play.go-dev.shuijingwanwq.com:8443/compile
https://play.go-dev.shuijingwanwq.com:8443/fmt
而没有配置外部基础地址时仍然保持:
/_/compile
/_/fmt
四、为什么外部接口没有继续使用 /_/
这里中途还做过一次小调整。
最开始为了尽量兼容原来的 A Tour of Go 请求路径,ZgoCloud 上也暂时设计成:
/_/compile
/_/fmt
后来重新看了一下官方接口:
go.dev
→ /_/compile
→ /_/fmt
而 Playground 的直接接口实际上是:
play.golang.org/compile
play.golang.org/fmt
因此新的独立执行服务没有必要继续继承 /_/ 这一层路径。
最终对外接口统一成:
/compile
/fmt
这样整个结构更加直接:
play.go-dev.shuijingwanwq.com:8443/compile
→ play.golang.org/compile
以及:
play.go-dev.shuijingwanwq.com:8443/fmt
→ play.golang.org/fmt
旧的同源接口则继续保持:
/_/compile
/_/fmt
两套接口的职责也因此更加容易区分。
五、Run 和两处 Format 必须统一切换
修改完成以后,生产环境中的三处请求都使用同一个基础 URL 规则。
Run:
https://play.go-dev.shuijingwanwq.com:8443/compile?backend=...
Playground 编辑器中的 Format:
https://play.go-dev.shuijingwanwq.com:8443/fmt?backend=...
课程页面服务中的 Format:
https://play.go-dev.shuijingwanwq.com:8443/fmt
其中:
backend
仍然只是一个普通查询参数。
它不会参与:
scheme
host
port
的选择,因此也不能利用这个参数改变请求目标。
六、生产基础地址还要考虑 JavaScript 字符串安全
这一轮还顺便补上了一个很容易忽略的问题。
生产基础 URL 最终会进入:
window.playgroundBaseURL = ...
因此不能简单地使用字符串拼接把配置直接塞进 JavaScript。
最终使用 Go 的:
encoding/json.Marshal
生成 JavaScript 可以安全使用的 JSON 字符串字面量。
测试中专门使用了类似:
https://example.test/";alert(1)//</script>
这样的特殊值进行验证。
json.Marshal 会正确处理:
"
\
<
>
&
等字符。
例如:
<
→ \u003c
>
→ \u003e
中途还因为测试手写的转义期望遗漏了 > 的:
\u003e
导致一次测试失败。
最后确认实现本身没有问题,只修正了测试,让测试直接根据 json.Marshal 的实际输出进行验证。
七、旧服务端 Playground 接口暂时没有删除
虽然生产浏览器现在已经准备直接访问 ZgoCloud,但我并没有马上删除阿里云 Go Tour 中原来的:
/_/compile
/_/fmt
它们仍然保留:
浏览器或其他兼容请求
→ 阿里云 Go Tour
→ go.dev
这不是正常生产 Run / Format 的主路径了,而是暂时作为:
兼容
回滚
能力保留。
这次迁移的重点是:
先把新的浏览器执行链路稳定上线,再考虑后续是否删除旧实现。
这样如果新链路上线后出现意外问题,仍然有比较简单的恢复空间。
八、完整测试通过以后才提交
这一轮最终一共修改了 9 个文件:
_content/js/playground.js
_content/tour/static/js/page.js
_content/tour/static/js/services.js
internal/tour/appengine.go
internal/tour/local.go
internal/tour/production.go
internal/tour/production_test.go
internal/tour/server_test.go
internal/tour/tour.go
测试包括:
生产注入正确的 Playground 基础 URL
本地 preview 不注入生产 URL
生产继续使用 HTTPTransport
本地继续使用 SocketTransport
生产 /socket 继续返回 404
Run 最终生成 ZgoCloud /compile
两处 Format 最终使用 ZgoCloud /fmt
空基础 URL 时仍保持 /_/compile 与 /_/fmt
backend 不能改变请求主机
JavaScript 字符串安全转义
最终:
go test ./internal/tour
通过。
完整:
go test ./...
也全部通过。
提交为:
ffbb8ce
feat: 生产浏览器直连 Playground 代理
九、release manifest 也必须同步更新
代码切换完成以后,我又发现了一个发布层面的遗留。
release.json 原来仍然记录:
{
"execution_transport": "http-playground-proxy",
"execution_provider": "go.dev"
}
现在正常执行最终已经落到:
play.golang.org
所以:
execution_provider
不能继续写成:
go.dev
最终调整为:
{
"execution_transport": "http-playground-proxy",
"execution_provider": "play.golang.org",
"local_socket_enabled": false
}
这里:
execution_transport = http-playground-proxy
仍然保留。
因为浏览器使用的依然是 HTTPTransport,并通过固定 Playground 转发节点调用官方执行服务。
同时还必须修改:
scripts/deploy-production.sh
因为部署脚本之前也会校验:
execution_provider = go.dev
如果只改 manifest 而不改部署预检,新 release 会被自己的生产部署脚本拒绝。
这一轮同步修改了:
publish.go
publish_test.go
deploy-production.sh
README.md
PROJECT_STATE.md
并再次完成完整测试。
十、生成新的 production bundle
最终 production bundle 为:
/tmp/go-tour-release-20260813-zh-CN-1fe73f5
对应项目 commit:
1fe73f54615b3d05d09133f6e25aa91c5fb07f75
发布状态:
ready=103
pending=0
blocked=0
pages=103
articles=7
release.json 中已经正确记录:
{
"execution_transport": "http-playground-proxy",
"execution_provider": "play.golang.org",
"local_socket_enabled": false
}
所有 bundle 文件重新完成 SHA-256 校验。
生产二进制仍然是:
Linux amd64
静态链接
CGO_ENABLED=0
十一、自动部署成功
新的 release 随后通过现有自动部署脚本正式部署。
最终生产目录:
/data/go-tour/releases/20260813-zh-CN-1fe73f5
部署过程完成:
本地 bundle 预检
远端上传
远端 SHA-256 校验
权限归一化
current 原子切换
go-tour.service 重启
localhost 健康检查
公网正式域名验收
服务刚重启时第一次健康检查出现:
service=active
HTTP=000
随后:
active + HTTP 200
active + HTTP 200
active + HTTP 200
连续三次通过后才正式判定部署成功。
最终:
deployment completed
public acceptance passed
到这里,从服务端看,新 release 已经成功上线。
但真正有意思的问题才刚刚出现。
十二、新 release 已经上线,浏览器为什么还在请求旧接口
我随后打开 Firefox,进入:
https://go-dev.shuijingwanwq.com/tour/welcome/1
打开开发者工具 Network 面板,然后点击“运行”。
页面能够正常输出:
Hello, 世界
程序已退出。
但是 Network 中显示的请求却仍然是:
POST https://go-dev.shuijingwanwq.com/_/compile?backend=

/_/compile这就很奇怪了。
因为:
- 新代码已经提交;
- 新 production bundle 已重新生成;
current已经切换;go-tour.service已重启;- 公网页面已经返回 200。
如果只看部署结果,很容易继续怀疑:
是不是生产 binary 没更新?
是不是 JS 没被打进 bundle?
是不是 initScript 注入失败?
但真正的原因并不在 Go Tour。
十三、真正的问题是 EdgeOne 缓存了旧的 /tour/script.js
A Tour of Go 的浏览器运行逻辑最终都来自:
/tour/script.js
新的 production release 虽然已经切换,但是 EdgeOne 节点仍然可能保存着之前版本的这个脚本。
因此浏览器拿到的是:
旧 HTML / 新服务
+
旧 script.js
旧 JavaScript 自然还会执行:
/_/compile
/_/fmt
最后确认问题以后,我没有清空整个站点缓存,而是只针对:
https://go-dev.shuijingwanwq.com/tour/script.js
进行定向缓存清除。

/tour/script.js 缓存这一点以后很值得记住:
production release 已经成功切换,并不等于浏览器一定已经执行新前端代码。
只要 CDN 上还保留旧的动态脚本资源,浏览器看到的行为仍然可能属于上一版本。
十四、刷新 /tour/script.js 后,新执行链路立即生效
EdgeOne 定向清除缓存以后,我重新加载课程页面。
再次点击“运行”。
这一次 Firefox Network 已经变成:
POST https://play.go-dev.shuijingwanwq.com:8443/compile?backend=
状态:
HTTP 200
协议:
HTTP/2
页面输出仍然正常:
Hello, 世界
程序已退出。

/tour/script.js 缓存后,Run 已直接请求 ZgoCloud /compile 并返回 HTTP 200响应中的 CORS 也符合预期:
access-control-allow-origin:
https://go-dev.shuijingwanwq.com
到这一刻,才真正能够确认:
production 浏览器 Run 已经不再经过阿里云 Go Tour 的
/_/compile。
十五、Format 也完成了真实浏览器切换
Run 通过以后,我又继续测试了“格式化”。
Format 的实际请求已经变为:
https://play.go-dev.shuijingwanwq.com:8443/fmt
同样能够成功返回。
因此最终两条正常 production 执行链路分别是:
Run
浏览器
→ ZgoCloud /compile
→ play.golang.org/compile
以及:
Format
浏览器
→ ZgoCloud /fmt
→ play.golang.org/fmt
这一步很重要。
因为前面代码审计已经发现,Run 与 Format 并不是只有一个统一请求点。
只有三处浏览器请求都正确切换以后,整个执行路径迁移才算完整。
十六、现在页面流量与执行流量真正分开了
完成这次迁移以后,生产环境实际上形成了两条完全不同的链路。
普通页面访问:
浏览器
→ EdgeOne
→ 阿里云 Nginx
→ Go Tour
代码 Run:
浏览器
→ play.go-dev.shuijingwanwq.com:8443/compile
→ ZgoCloud Nginx
→ play.golang.org/compile
Format:
浏览器
→ play.go-dev.shuijingwanwq.com:8443/fmt
→ ZgoCloud Nginx
→ play.golang.org/fmt
也就是说:
阿里云现在主要负责页面服务,不再承担正常 Run / Format 的远程 Playground 转发。
生产服务器依旧不会在本机执行用户提交的 Go 程序。
十七、手机真实公网测试的体感也明显改善
桌面 Firefox 验收完成以后,我又直接在手机端进行了测试。
这次没有使用额外的网络工具。
直接打开生产版 A Tour of Go,然后点击“运行”。
实际体验是:
点击以后,运行结果基本很快就返回了。
这个结果对我来说很有意义。
因为服务器端的:
curl
测试只能证明:
ZgoCloud
→ play.golang.org
工作正常。
桌面 Network 则进一步证明:
浏览器
→ ZgoCloud
已经真实生效。
手机端真实网络环境的实际使用,则进一步验证了这条完整路径:
手机浏览器
→ ZgoCloud
→ play.golang.org
在本次测试环境下确实能够快速返回。
当然,一次真实使用并不能证明任何地区、任何网络、任何时间都一定保持相同表现。
但结合:
ZgoCloud 100 轮连续测试
桌面 Firefox HTTP/2 验收
手机真实公网体验
已经有多层证据说明这次调整方向是有效的。
十八、旧阿里云执行接口暂时继续保留
目前阿里云 Go Tour 中仍然保留:
/_/compile
/_/fmt
以及对应的:
https://go.dev/_
服务端转发逻辑。
但正常 production 浏览器已经不会再使用它们。
我目前没有急着删除。
原因很简单:
已经稳定运行的兼容和回滚能力,没有必要在新链路刚上线时立即清理。
等新的执行链路运行一段时间以后,再决定是否进一步做减法会更加稳妥。
所以现在的状态是:
新链路:
正式使用
旧链路:
保留,但不作为正常浏览器执行路径
十九、这次最值得记住的不是代码,而是 CDN 缓存
这次代码本身的修改其实并不算复杂。
真正容易让人误判的是:
Git commit 成功
→ production bundle 成功
→ 自动部署成功
→ current 已切换
→ service 已重启
→ 正式站点 HTTP 200
这些全部成功以后:
浏览器仍然可能执行旧代码。
因为用户真正运行的 JavaScript 还经过 CDN。
因此以后只要发布内容涉及:
/tour/script.js
或者其他决定前端行为的资源,就不能只看:
systemd
curl localhost
正式首页 HTTP 状态码
还必须打开浏览器 Network,确认:
实际请求到底发到了哪里。
这次如果没有检查 Network,只看页面输出:
Hello, 世界
其实根本发现不了执行链路仍然是旧的。
二十、最终状态
截至这次发布完成,正式 production release 为:
/data/go-tour/releases/20260813-zh-CN-1fe73f5
对应代码:
1fe73f54615b3d05d09133f6e25aa91c5fb07f75
发布状态:
ready=103
pending=0
blocked=0
pages=103
articles=7
release manifest:
execution_transport=http-playground-proxy
execution_provider=play.golang.org
local_socket_enabled=false
真实生产验收:
Run → ZgoCloud /compile → HTTP 200
Format → ZgoCloud /fmt → 成功
CORS → 正常
/socket → 404
手机真实公网 Run:
成功
且本次体验中响应较快
至此:
A Tour of Go 生产 Playground 执行链路迁移至 ZgoCloud 固定执行节点已经完成。
总结
这次迁移从表面上看,只是把:
/_/compile
/_/fmt
改成:
https://play.go-dev.shuijingwanwq.com:8443/compile
https://play.go-dev.shuijingwanwq.com:8443/fmt
但真正做完整以后,涉及的其实是:
现有生产请求链路审计
生产与本地运行模式区分
Run 与两处 Format 请求统一
JavaScript 配置安全注入
CORS
release manifest
生产部署预检
production bundle
自动部署
CDN 缓存
浏览器 Network 验收
手机真实使用验收
最终得到的架构也比原来更加清晰。
页面负责页面:
浏览器
→ EdgeOne
→ 阿里云
→ Go Tour
执行负责执行:
浏览器
→ ZgoCloud
→ play.golang.org
两部分不再强绑定在同一条网络链路上。
而这次上线过程中最有价值的一条经验,我认为反而不是某一段代码,而是:
前端行为发生变化时,production 服务部署成功并不代表用户已经拿到新代码。最终验收必须落到浏览器 Network 中的真实请求上。
只有看到:
https://play.go-dev.shuijingwanwq.com:8443/compile
真正出现在浏览器请求列表里,这次迁移才算真正完成。
A Tour of Go 多语言翻译项目
本系列完整记录 A Tour of Go 多语言翻译项目从架构设计、整页翻译、结构保护、自动校验,到生产发布与后续维护的实际开发过程。
项目入口:
✅ 在线学习:A Tour of Go 简体中文版
✅ 项目源码:GitHub:shuijingwan/go-tour-i18n
当前第一阶段已完成简体中文版本。项目属于非官方社区多语言翻译项目,与 Go 官方无隶属或授权关系。


发表回复