本文 首发于 🌱 煎茶转载 请注明 来源

来源:Just Fucking Use Go - Blain Smith

说明:本文为 AIGC 辅助翻译整理版本(对原文各段落做了转述与浓缩,非逐句全文翻译),已进行简单人工校对,具体细节请自行甄别,建议参阅原文。

别墨迹了,用 Go 就完事了

文章标题原文相当直给:两秒编译完成、打包成单个二进制文件、凌晨三点某个 npm 依赖被撤包也不会让你的服务崩掉——作者说的就是 Go。就像 HTML 早就摆在那里等前端别再自己找罪受一样,Go 也已经在那儿蹲了十几年,等着后端工程师别再把简单问题复杂化。

但现实是,很多团队还在用十几个 Node 包、三套 TypeScript 构建工具,外加一整套 Kubernetes 集群,去撑起一个普普通通的表单提交。有人专门养了一个平台团队去伺候 Rails 单体应用,也有人说服 CTO 用 Rust 重写一个每秒撑死几十个请求的 CRUD 应用。作者的评价很直接:这是自己给自己挖坑。

“无聊"是设计出来的

Go 语言读起来"无聊”,作者认为这正是它的优点——没有装饰器、没有元类、没有宏,也没有 Haskell 圈子里那些让人头晕的 trait、monad。只有结构体、函数、接口、goroutine 和 channel。语言规范可以在午休时间读完,下午就能上手写代码。

“无聊"意味着新人能看懂两年前资深工程师写的代码;gofmt 已经统一了格式,团队里没人能靠"炫技"往代码库里塞十七层抽象——因为语言本身不允许。

标准库就是框架

作者的态度很硬:别找框架了,标准库本身就是框架。文章给出了一个用 net/httphtml/templateembed 写成的最小 Web 应用示例——HTML 模板直接编译进二进制文件,没有 webpack、没有 Vite、没有体积堪比一辆汽车的 node_modulesgo build 之后就是一个可执行文件,扔到服务器上就能跑。

数据库用 database/sql,JSON 用 encoding/json,想调用其他服务照样用 net/http(它同时也是客户端),想并发就在函数调用前加一个 go 关键字,测试用 go test,基准测试用 go test -bench,性能分析工具 pprof 早就内置好了。

标准库的深度也体现在几个关键抽象上:io.Reader/io.Writer 这两个各只有一个方法的接口,撑起了整个生态里"管道式"处理数据流的能力;context.Context 负责统一取消信号的传递——用户关掉浏览器标签页,HTTP 请求、数据库查询、下游调用会一路跟着取消,不会留下泄漏的 goroutine 或吃满连接池的僵尸查询;而 encoding/jsonencoding/xmlencoding/csv 等编码包用的是同一套 struct tag 和"解码进指针"的写法,学会一个基本就等于都会了。

并发不用哭着写

Goroutine 不是线程,它是由运行时调度、复用在系统线程之上的有栈协程,启动成本大约 2KB,一台笔记本上开十万个都不成问题。文章举了一个用 channel 并行抓取多个 URL 的例子:给每个请求起一个 goroutine,把结果丢进 channel,主流程再依次读出来——没有额外的库,没有 async/await 的仪式感,语言原生就把这件事做了。

一个不是 Hello World 的例子

作者贴出一段真实感更强的代码:一个从 Postgres 读数据、渲染 HTML 的 CRUD 路由,把数据库查询、模板渲染和 HTTP handler 全部塞进一屏代码里,请求的 context 一路传到 SQL 查询,连接一断查询就跟着取消。没有 ORM,没有依赖注入容器,没有 service 层,也没有塞满一堆抽象基类的 controllers/ 目录——从头读到尾就知道它在干什么。

依赖管理不折腾人

go mod init 之后,依赖就活在 go.modgo.sum 两个文件里,后者是你实际拿到的依赖内容的哈希记录,一旦有人悄悄替换包内容就能被发现。没有体积惊人的 node_modules,没有开发环境和 CI 之间的 lockfile 漂移,也没有 peerDependencies、devDependencies 那一堆概念。需要离线构建就用 go mod vendor,把全部依赖打进 vendor/ 目录,工具链自动识别,整个项目连同依赖打成一个 tar 包就能带走。

工具链是编译器自带的

gofmt 统一代码格式,没有类似 .prettierrc 那样的风格之争;go vet 挑出明显的错误;go test 跑测试,加上 -race 参数就能带着竞态检测器跑,-bench 做基准测试,-cover 看覆盖率;go tool pprof 能从正在运行的线上服务里,通过两行代码接入的 HTTP 端点直接拉出 CPU 和内存的火焰图。这些统统是标准工具链自带的能力,不用装插件,也不用维护额外的配置文件。

部署就是一条 copy 命令

这大概是最让 Rails / Node 阵营破防的部分:编译出一个 Go 二进制文件,scp 到服务器,systemctl restart 一下,三条命令,部署完成。没有 Dockerfile,没有多阶段构建,没有每周准时弹出的基础镜像 CVE 警报,没有 Kubernetes manifest、没有 Helm chart、没有 ArgoCD、没有 service mesh、没有 sidecar。一个十几 MB 的静态链接二进制文件加一份二十行的 systemd 配置,就是一次能用很多年的生产部署。

“那 Rails / Django / Express / Next 呢?”

作者一一吐槽:Rails 的部署要靠 Capistrano、一堆配置文件外加运气;Django 要求你学它的 ORM、admin、中间件体系以及一整套设计哲学;Express 靠 npm audit 的警告和祈祷维持运转;Next.js 则是隔三差五换一次路由约定,还一副理所当然的样子。相比之下,Go 编译出来的二进制文件不挑环境,五年后大概率还能在当时还不存在的硬件上继续跑。

“那微服务呢?”

作者的回答很干脆:不需要。先老老实实写一个 Go 单体应用,配一个 Postgres,顶多再加一个 Redis;HTML 页面和 JSON API 走同一个端口;跑在一台比每月奶咖开销还便宜的 VPS 上,扛到每秒一万请求都不会吃力,因为 Go 本身就是为这种场景设计的,goroutine 足够廉价。真到了需要拆分的那一天(作者认为多数团队根本用不上),拆一个 Go 单体应用无非是把已经存在的包挪进各自的仓库——接口边界早就在那儿了,是语言本身逼着你这么设计的。

“那泛型呢?那没有异常的错误处理呢?”

在作者看来,if err != nil 是特性而不是缺陷:它逼着你正视每一个可能出错的地方,并当场决定怎么处理,而不是像层层嵌套的 try/catch 那样把错误一路藏到深夜的生产事故里才暴露出来。至于泛型,Go 1.18 就已经加上了,需要的时候用就是了,没什么好纠结的。

别墨迹了,用 Go 就完事了

作者的结论很简单:不需要框架,也不需要微服务、Rust 重写,或者上周才发布、号称能拯救一切的 JavaScript 元框架。打开编辑器,go mod init,写一个 main.go,把模板嵌进去,编译,发布——“无聊"的选择,往往就是对的选择。