最近在翻十年没动过的 Go 源码,翻到一个叫 lhttp 的老项目。它做的事是把 HTTP 的报文格式搬到 WebSocket 上,命令行、headers、空行、body,跟裸 HTTP 请求差不多;多个服务器之间用 NATS 协调,部署一组就能撑起一套聊天服务,不需要依赖 XMPP。这篇讲讲我把它编译起来的过程,以及跑起来之后遇到的那几个坑,读完你应该能省下小半天摸索时间。
拆开看看。2016 年的 Go 项目基本沿用 Godeps 加上 vendor 目录,那年头还没有 module 的概念。仓库里那份 Godeps.json 能看到两个关键信息:GoVersion 声明的是 go1.6,依赖清单里打包了 gnatsd:
{
"GoVersion": "go1.6",
"Deps": [
{
"ImportPath": "github.com/nats-io/gnatsd"
}
]
}
依赖全部 vendored 进仓库意味着不需要联网拉取,我用本机的 Go 1.24 在 GOPATH 模式下直接编过了,零改动:
GO111MODULE=off go build ./...
一个 2016 年的项目,能在 2026 年的工具链上零改动编过,最大的功臣是 vendored 依赖——不联网、不走 module 拉新版本,向后兼容的压力小很多。但跑起来就是另一回事了。文档说要暴露 8081 端口,浏览器却一直连不上,翻源码才发现监听的是 8581:
const PORT = "8581"
前端连 8081,服务器听 8581,中间没有任何日志提示你哪里断了。这类文档和源码的裂缝,放到今天的 code review 里大概留不住,但十年前就一直躺在那里。
跑通 demo 之后试了试发消息,发一条纯文本,服务器回了一个 auth 命令,Content-Type 还是 image/png:
auth
Content-Type: image/png
看了一圈,是 demo 的 ChatProcessor 里留了个调试响应没删,把测试用的命令当正常消息发了出来。
值得称赞的是 subpub 消息通道本身是好的,通过 NATS 扇出到订阅者,一路验证下来没有意外。但发送者会收到自己的消息两次:chat 命令的 handler 既直接回了一条,又把消息重新发布到它自己也订阅着的 NATS 频道上,于是自己那条被投递了两遍。这类运行时交互的缺陷,靠 diff 式的代码审查基本看不出来。
顺手跑了一个请求/响应往返的基准,单线程 Python 客户端,一万个往返大约 17900 条/秒。文档里记的 2016 年基准是单核 1GB 内存下 1 万次发布 0.04 秒——纯发布/订阅场景的吞吐,NATS 从当年到现在一直相当够用。
划重点:第一,2016 年的 vendored Go 代码在 2026 年的工具链上零改动编译通过,Go 向后兼容确实做得好,但前提是依赖被锁在仓库里,换 module 模式拉新依赖未必有这个运气;第二,老文档里的端口号先别信,连不上优先读源码;第三,lhttp 不能再用于新项目了,它依赖的 WebSocket 库过时是硬伤,但消息通道的设计到今天仍然值得拆开看看。这条记下来了,下次应该用得上——你也可以拿自己手里最老的那个项目试着编一遍,看看能不能跑起来。
