最近我陆续把一个 1K DAU 的小产品——“咕咕同步”——从 Node.js 重写为 Go,并全线开源了。

问题

每天写长文或对话时,我要么坐在电脑前,要么靠在床上。真正难受的是这种场景:手机靠近嘴边,用任意输入法(讯飞、搜狗、iOS 原生)小声语音输入,文字需要立刻出现在电脑上

已有的方案各有问题:

  • 微信灌输入法太重,还带广告
  • Google Docs / Notion 同步有延迟,打电话时不能用
  • 要么是走 ASR 云服务——我并没有转写需求,输入法已经帮我做了

需要的是一个纯同步通道:手机发什么,PC 立刻收到什么。

做法

协议很简单:WebSocket 房间制,手机和 PC 扫同一个二维码进"房间",全量文本 + 光标位置(p)实时互发。不走 OT,不走 CRDT。

后端用 Go + gorilla/websocket。一开始我是用 Node.js 写的,在生产跑了一个月,发现:

  • PM2 管理的 Node 进程内存泄漏不明显但有点烦
  • goroutine 模型做连接管理比 Event Loop 清晰太多了
  • 单二进制部署,systemd 直接管,零运行时依赖

关键决策:

  • 消息是完整文本,不是 diff(文本本身就在手机端编辑器里,同步成本零)
  • 心跳 3s ping / 6s pong——我从 30s 误设为 2s 最终调优到 3/6,太紧会把移动端 radio 吵醒耗电
  • 房间 TTL 30 分钟,没人自动回收
  • 限速 10 conn/min + 100 msg/min per IP,放公网基本够用

前端零构建。HTML + CSS + JS 分离,改样式读 CSS、改逻辑读 JS,比打包一大坨明白太多了。

架构图

手机 (IME 语音输入)
  → WebSocket 发送 {"t":"u","d":"完整文本","p":光标位置}
      ↓ (Go server: 单房间广播)
PC (浏览器 textarea)
  → 远端接收 → applyRemoteText + setSelectionRange(p, p)

无 Redis,无数据库,无消息队列。房间在内存,进程重启自然清空。

成果

  • 单二进制约 9.7 MB,systemd 一键启停
  • htop 看 RSS 稳定 ~50MB,没发现漂移
  • 高峰期 ~100 CCU 无压力
  • 全链路延迟肉眼 < 100ms(从手机抬起到 PC 上出现文字)

开源

我把完整代码放在了 GitHub(https://github.com/NoelJudeNoel/sync-pad),欢迎有类似"需要轻量实时同步通道"场景的同学 fork,改完 WebSocket 后端做自己的协议层就行。

如果不是这个需求,我可能会选 Firebase / Supabase Realtime 更省力。但既然我对延迟和架构有洁癖,Go 这条路走下来是值得的。


项目地址、部署 Node.js → Go 的完整 diff 在 GitHub 仓库里,欢迎精读代码找 bug。