就用Go
摘要
一篇带有强烈观点的开发者文章倡导使用Go编程语言,强调其简洁的语法、强大的标准库、高效的并发模型以及单二进制部署,作为对过于复杂的现代技术栈的实用替代方案。
<p><a href="https://lobste.rs/s/znlkib/just_fucking_use_go">评论</a></p>
查看缓存全文
缓存时间: 2026/05/08 14:29
# 给我他妈用 Go - Blain Smith
来源:https://blainsmith.com/articles/just-fucking-use-go/
2026年5月8日
嘿,蠢货。你知道什么东西编译只要两秒、部署为单个二进制文件、而且不会因为凌晨三点 npm 上某个传递依赖被删就炸掉吗?Go。就像HTML从该死的互联网诞生之初就一直存在(https://justfuckingusehtml.com/)等着你停止把前端搞复杂一样,Go 也已经存在了十多年,等着你停止把后端搞复杂。
但你没有。你正在把十五个 Node 包、三个 TypeScript 构建工具和一个 Kubernetes 集群拼在一起,就为了服务一个他妈的表单。你雇了一个平台团队来保姆你的 Rails 单体应用。你说服你的 CTO 说,对于一个每秒可能只有四十个请求的 CRUD 应用,Rust 是必需的。恭喜你,混蛋。你把自己坑了。
## 这门语言故意很无聊
你知道为什么 Go 感觉无聊吗?因为它就是无聊,而这他妈就是重点。没有装饰器。没有元类。没有宏。没有特型、单子,或者 Haskell 这帮人这周正在吸食的任何被诅咒的抽象。有结构体、函数、接口、goroutine 和 channel。就这些。你可以在午餐休息时读完规范,并在当天下午就能干活。
无聊意味着你上个月招的初级员工能读懂首席两年写的代码。格式化只有一种方式,而 `gofmt` 已经替你做了。你那“聪明”的同事没法在代码库里偷偷塞进十七层抽象,因为语言不允许他这么做。当没人对着自己的小聪明流口水时,这就是交付的样子。
## 标准库就是框架
别找框架了,你个绝对的核桃。
标准库*就是*框架。
```go
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
```
这是一个能运行的 Web 应用。HTML 模板编译进了二进制文件里。没有 Webpack。没有 Vite。没有“开发服务器”。没有他妈的大众汽车那么大的 `node_modules`。你 `go build`,然后交付一个文件。丢到服务器上。搞定。
你想要数据库?`database/sql`。JSON?`encoding/json`。想和另一个服务通信?`net/http` 也是一个客户端。想同时做五件事?前面加个 `go`。测试?`go test`。基准测试?`go test -bench`。性能分析?`pprof` 已经在那里嘲笑你的 `console.log` 调试了。
## 标准库也他妈很深
`io.Reader` 和 `io.Writer` 是两个各有一个方法的接口。这也是为什么你可以用三行代码把 HTTP 响应体通过 gzip writer 管道传输到磁盘文件上,而不用动脑子。生态系统中每一个正经的包都使用它们。一旦你掌握了这一点,Go 一半的“魔法”就只是这两个接口到处出现而已。
`context.Context` 是用来取消操作的。用户关闭浏览器标签页,请求上下文取消,数据库查询取消,下游 HTTP 调用取消。一路下去。没有泄露的 goroutine。没有消耗连接池的僵尸查询。你把它作为第一个参数传递,并且尊重它。整个 API 就是这样。
`encoding/json`、`encoding/xml`、`encoding/csv`、`encoding/binary`,都在标准库里。相同的结构体标签模式。相同的解码到指针的体验。学会一个,基本上就全会了。
## 不会让你哭的并发
Goroutine 不是线程。它们是栈式的,由运行时多路复用到 OS 线程上,启动成本大约 2KB。你可以在笔记本电脑上生成十万个。试试用你的 Node 事件循环做同样的事,然后看着它拉一裤兜子。
Channel 是 goroutine 之间带类型的管道。你在一端发送,在另一端接收,运行时负责同步。如果你需要共享状态,`sync.Mutex` 就在那里,而竞态检测器会在你搞砸时告诉你。
```go
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
```
这是一个并行的 HTTP 获取器。没有库,没有框架,没有 async/await 仪式。语言本身就做到了。
## 一个真实的例子,不是 hello-world
下面是一个从 Postgres 读取数据并渲染 HTML 的 CRUD 路由。完整代码。
```go
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
```
数据库、模板和一个 HTTP 处理器,都在一个屏幕里。请求上下文通过查询传递,这样关闭的连接会取消 SQL。没有 ORM,没有 DI 容器,没有服务层,没有包含十七个抽象基类的 `controllers/` 目录。你可以从上到下阅读,并且确切知道它在做什么。
## 不会毁了你周末的依赖管理
`go mod init`。搞定。你的依赖在 `go.mod` 和 `go.sum` 里。sum 文件是你实际得到内容的加密记录,所以当有人对你搞 left-pad 时你能发现。没有 `node_modules` 目录。没有开发环境和 CI 之间的锁文件漂移。没有同级依赖、可选依赖、devDependencies、peerDependenciesMeta。只有一个文件列出你用的东西,一个文件证明你得到了你期望的。
你想要离线构建?`go mod vendor` 把所有东西丢进 `vendor/` 目录,工具链自动使用它。整个项目,包括依赖,都能放进一个 tarball。你的安全团队会感激涕零。
`gofmt` 格式化你的代码。没有争议。没有 `.prettierrc` 的圣战。格式就是格式,所有人都用它。你的差异保持很小,因为没人会调整空白。
`go vet` 捕获明显的错误。`go test` 运行你的测试。`go test -race` 用竞态检测器运行,找出你以为没有的数据竞争。`go test -bench` 运行基准测试。`go test -cover` 告诉你漏掉了什么。`go tool pprof` 通过你两行代码就能接上的 HTTP 端点,给出来自运行中生产服务的 CPU 和内存使用火焰图。
这些都不是第三方工具,都不是插件,都不需要你维护的配置文件。它们都在盒子里。
## 部署就是一个复制命令
这是让 Rails 和 Node 的人物理上感到愤怒的部分。你构建一个 Go 二进制文件。你把它复制到服务器上。你运行它。
```bash
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
```
三个命令。搞定。没有 Dockerfile。没有多阶段构建。没有每周二的基础镜像 CVE 警报。没有 Kubernetes 清单文件。没有 Helm chart。没有 ArgoCD。没有服务网格。没有 sidecar。
一个 12MB 的静态链接二进制文件和一个 20 行的 systemd 单元文件就是一个生产部署。它会活得比你的职业生涯长。你唯一需要用 Docker 的原因是如果你的运维团队合同里规定必须用它,即便如此你也可以把二进制文件塞进 `FROM scratch` 镜像里,然后收工。
## “但是 Rails / Django / Express / Next 呢?”
它们怎么了?Rails 需要一个涉及 Capistrano、三个配置文件、一只山羊的部署仪式。Django 要你学习它的 ORM、管理后台、中间件系统,以及关于一切的观点。Express 靠 `npm audit` 警告和祈祷撑着。Next.js 每六个月改变一次路由惯例,并为此对你进行煤气灯效应。
你的 Go 二进制文件不在乎。它编译了,运行了,并且五年后在不存在的硬件上仍然能运行。你的框架?圣诞节前就废弃了,维护者会写一篇关于倦怠的 Medium 文章。
## “但是微服务!”
不。
写他妈的单体应用。一个 Go 二进制文件。一个 Postgres。一个 Redis,如果你绝对需要的话。在同一个端口上提供 HTML 和 JSON API。运行在一个单价低于你每月燕麦奶预算的 VPS 上。毫不费力扩展到每秒一万个请求,因为 Go 就是为此设计的,而且 goroutine 便宜得要命。
当你真的需要拆分它时——而且你不会需要——拆分 Go 单体应用只是把包移到它们自己的仓库里。接口已经在那里了。你没有刻意设计就做到了,因为语言迫使你这么做了。
## “但是泛型!但是错误处理!但是没有异常!”
`if err != nil` 是特性,不是 bug。它迫使你查看每一个可能出错的地方,并且*决定*该怎么做。你那 try/catch 嵌套地狱并没有让错误消失,它只是把它们藏起来,直到凌晨两点的生产环境爆发。
泛型在 1.18 中落地了。它们很好。需要时就用它们。别抱怨了。
## 给我他妈用 Go
别假装你需要一个框架了。你也不需要微服务,或者 Rust 重写,或者上周二发布的任何能拯救你——而前六个没能做到——的 JavaScript 元框架。
打开你的编辑器。运行 `go mod init`,写一个 `main.go`,嵌入你的模板,然后编译。把那该死的东西交付出去。
无聊的选择是正确的选择。一直如此。
相似文章
就他妈用 React
这篇主观性很强的文章激进地主张在复杂 Web 应用中使用现代 JavaScript 框架(如 React)而非纯 HTML,认为复杂性需要合适的工具。
优化CPU密集型Go热路径的笔记
本文讨论了CPU密集型Go代码的性能优化技术,指出了泛型和接口抽象因无法内联而产生的局限性,并主张在热路径中使用代码复制。文章通过一个Brotli移植示例和深入基准测试进行了说明。
Go 语言服务器可以实现令人印象深刻的代码导航
Go 语言服务器 (gopls) 为 Go 开发者提供了令人印象深刻的代码导航功能,增强了 IDE 的能力。
Go 实验详解
本文介绍了 Go 语言中实验性功能的处理方式、生命周期以及近期实验示例。
从Go迁移到Rust
一份为Go开发者迁移到Rust编写的全面指南,专注于后端服务,对比正确性、运行时和人体工程学方面的权衡,并提供关于渐进式迁移的实用建议。