Go 语言初体验:Less is more,一种丑但可靠的工程美学
AI Summary
最近新项目评估技术栈,因为 Java/Kotlin 太重,TypeScript/JavaScript(Node.js)因为 js 原型链的问题我一直感觉不是合格的后端语言,写 Rust 的话社区根本不会有几个人贡献,再加上后台任务,轻量化,易于部署,可能就只有 Go 能担任这个职责了。
其实我之前用过 go 的,当时在搞 AI 原型生成器的时候,为了快捷操作容器,我用 go 搞了个沙箱管理器,操作容器,对外 gRPC,利用了它在云原生领域的生态优势。而这次,我看中的是它易于入门,编译快,占用轻,易于部署,当然重要的是,协程模型确实很现代很舒服。
真的很丑
我对 Go 的第一印象非常稳定:丑。
对于习惯了 Java/JS 的注解(装饰器),Kotlin 的 DSL,Rust 的宏来说的我,Go 的语法极其贫瘠,真是可以说简陋。对于 Go 的显式哲学来说,不像是语言的搭积木,而是你把螺丝刀给我,我就能把家装起来,但别问我为什么这个螺丝长这样。
于是字符串成了注解
我们从这样一段例子开始:
go
为了做一个简单的序列化和参数校验,必须在结构体后面跟上一长串 json:"name" binding:"required,min=5"。为了数据库字段的对应和行为,又要写一长串关键词。这种把逻辑写在字符串里的做法感觉不知道梦回了哪个时代。但是原因也很简单嘛,因为没有注解/宏/DSL,只能用这种方式来表达。
指针定义的 Overloaded
Go 的显式很多时候不是清晰,而是盲目想要复用反而使得语义过载。
比如,你想要一个可选值?行,给你 *T。 但 *T 在 Go 里又不仅仅是 Optional——它同时还是:
- “这个字段可能为 NULL”(DB / JSON)
- “我想区分零值和未设置”(patch / update)
- “我想共享/引用同一份数据”(引用语义)
- “这个方法需要指针接收者”(行为语义)
那这就很可怕了,于是当你在 Go 的代码中看到一个*,你还需要费尽心力去琢磨是可空还是关系。而原因只是因为 Go 没有一个设计好的 Optional/Result。
同一个
*被迫承担了四种语义,结果是:代码显式了,意图却更隐式了。
错误处理变为传递责任链
然后是错误处理。
写 Go 的时候,键盘上最先磨损的永远是
i,f,e,r,n,l这几个键。
在 Kotlin 里你可能会用 runCatching,在 Rust 里你有 ?,在 Java 里至少异常处理也未尝不可,但是 Go:if err != nil { return err }
你可以说这很显式,很清晰,很正确的考虑了每一种可能分支。
但当你的业务开始出现一定的复杂度:超时、取消、重试、降级、后台任务、幂等、签名验签、缓存穿透……你会发现你写的不是后端,而是考虑所有,搭建了一条错误传播管道。
当然,最让人抓狂的是,这种繁琐并没有带来更好的安全性。它不像 Rust 的 Result<T, E> 那样强制你在编译期处理错误,也不像 Java 的 Checked Exception 那样有显式的签名约束。它只是一个约定,如果你忘了写这两行代码,那么发生什么边界情况就不可控了。
写业务的“地狱体验”
想要一个好用的 ORM
Go 的 ORM 生态有一种奇妙的割裂感:
要么极度魔法,要么极度朴素,中间那条舒适区间很窄。
这玩意儿除了名字叫 ORM,哪里像个现代 ORM 了?不如说是 SQL 拼接器
在 Kotlin Exposed 或者 Rust SeaORM 里,或者哪怕是(不属于 ORM)手写 SQL 的 Rust sqlx,他们都是强类型的,强大的编译时安全让写代码就很有底气。你写错一个字段名,编译器立马给你报错。但在 Go 里(尤其是 GORM),你又回到了拼接字符串的恐惧......
db.Where("user_nmae = ?", name).First(&user)—— 这里的user_nmae写错了?编译通过,运行报错!
而当你开始尝试 Ent,感受到 DSL 的舒服,编译安全,但它妄图掌控数据库的感觉,以及完全无法自己精细修改的表结构、定义的索引优化等等,都让人感觉这根本就是为社交关系服务的图数据库,它的抽象会强到让你觉得“我在写 Ent,不是在写业务”。
拜托,学习它的 SeaORM 都那么好用,人家尊重数据库,SQL 优先,利用 Rust 的语言特性搞了那么好的优化体验,Ent 居然能这么难用。
于是最后很多人回到朴素路线,手写 migration(goose),查询用 sqlx/sqlc,开始抱怨 Go 的 ORM 总有一种“隔靴搔痒”的无力感。它要么太灵活以至于不安全,要么太重型(靠大量代码生成)以至于繁琐。
用脚本补充的语言能力
Go 的精神很一致:语言保持小,复杂度交给工具链。
你可能会喜欢上 Rust 的宏展开代码,Kotlin/Rust 的 dsl 优雅美观,Java/JS 的注解轻松切面扩展。而 Go 呢?//go:generate。 它不是语言特性,它只是一个让工具链去跑个 shell 命令的“补丁”。
所以你会看到整个生态一大堆生成器驱动的解决方案:
- ORM 生成(Ent / sqlc)
- Mock 生成(mockgen)
- API client 生成(OpenAPI generator)
- Protobuf/gRPC 生成
- ...
于是项目里面的 Makefile 成为了最佳实践,成为了一切生成器的优雅入口。
这当然有好处:
生成出来的就是普通 Go 代码,可读、可调试、编译期安全。
然后你就会收获一种非常 Go 的痛苦:
- 你改了 schema,忘了 generate,CI 才告诉你
- 生成文件冲突,Git diff 像雪崩
- Debug 时你在你写的和生成的之间来回跳
然后只能告诉自己一句:
“这不是缺点,这是工程化。”
迟到的“半成品”
Go 的泛型给我的感觉很像——这辆车终于加了变速箱,但你一脚踩下去发现它只愿意在能跑这个层面负责,至于好不好开,你自己想办法。
1. Go 既然有了泛型,却依然不支持扩展方法(Extension Methods)。
即便有了泛型,你依然不能给切片加方法。于是官方标准库 slices 逼着你写成了这样: slices.Map(slices.DeleteFunc(list, func...), func...)
2. 只有约束,没有推导
Go 的泛型在使用上经常需要极其啰嗦的显式声明。明明编译器应该能推断出类型,但很多时候你还是得把那一长串 [TypeA, TypeB] 写出来,导致代码里充斥着方括号。
而且那个 any 关键字,说白了就是把 interface{} 换了个皮,并没有带来像 Rust 那样严格且强大的类型系统约束能力。你写出来的泛型代码,往往为了迁就 Go 那个并不聪明的编译器,变得比不写泛型还要难以阅读。
真的很稳:工业级的暴力美学
但话说回来,Go 的优点并不是它很美,而是它总能在你最需要的时候,干净利落地把活儿干完。我们可以看到他有那么多槽点,甚至这篇文章只列出了前 20%,想要讲述真正让我喜欢 Go 的,我们得换个角度——从“语言设计的艺术”转向“工程落地的暴力美学”。
能不能快启动、能不能少出事、能不能轻易被别人接手、能不能在一堆后台任务和边角脏活里不崩溃。Go 在这些方面,几乎就是工业界的低配答案,但往往是最正确的答案。
1)轻,是一种长期主义
Go 的轻是一种极其务实的取舍,你不需要把一天的情绪交给 Gradle、Maven、Cargo 或者 pnpm install 之后的依赖地狱。你不需要考虑沉重的 JVM,黑洞大小的 node_modules,一个二进制就轻松运行。
2)现代的协程模型,可以说在节省生命
在 Node.js 里,你得处理 Promise、async/await 传染性,一旦忘了 await 就像踩了雷;在 Rust 里,你得面对 Tokio 的运行时选择、Pin、Future 的生命周期……心智负担极重。
而 Go 的 Goroutine 是对开发者最友好的并发模型,没有之一:
go
就这一行,Go 运行时帮你解决了 M
的调度,帮你处理了上下文切换。你写的是线性的、符合直觉的同步代码,底层跑的确是高效的异步非阻塞逻辑。所以为什么我和朋友总会相互开玩笑,说 Go 工程师想的都是只要业务写完了,剩下的就爽了。让你面对真实需求的时候,Go 的 select 和 channel 让你能像搭积木一样优雅地控制并发,而不是陷入回调地狱或生命周期深渊。
3)“丑”的另一面,是可维护
说实话,Go 语法上的丑,很大一部分其实是 Go 的一种强行约束:别太聪明,他让代码逻辑绝对平铺,显式写出了一切。
- 没有宏 → 你没法把业务塞进编译期魔法里,接手的人能轻松读懂。
- 没有注解 → 你必须显式声明逻辑,代码更有可读性。
- 错误处理啰嗦 → 你很难忘记处理,也很难忽略掉业务里每一步的问题。
你可以随便招一个开发者,让他看两天文档,他写出来的代码虽然丑,但你一眼就能看懂他在干嘛。Review 代码不再需要脑补上下文和复杂的继承关系,所见即所得。
Go 的哲学在于,它强迫所有人都用最笨的方式写代码,从而消灭了奇技淫巧带来的维护成本。这在个人项目里可能不突出,但在多人协作和长期演进里,便让可维护性到了其他语言无法企及的地步。
4)交叉编译,DevOps 的终极梦想
这部分甚至无需多言。
bash
回车敲下,你会得到一个纯静态链接的二进制文件。 没有 node_modules 黑洞,没有 JVM 依赖,没有 glibc 版本冲突。 你把这个文件 scp 到服务器上,chmod +x,然后 ./server,就这么简单。
配合 Docker,你的 Dockerfile 可能只有 5 行:
Dockerfile
这对于我们这种“个人开发者”或“小团队”来说,省下的时间就是生命。
5)高效工具链,很爽的开发
Rust 编译一次可能够你喝杯 Java,Go 编译一次可能只够你眨几次 👀。 在微服务架构或者频繁迭代的开发流程中,这种极短的反馈回路(Code -> Run -> Test)带来的心流体验,足以抵消写 if err != nil 的烦躁,让你清晰记得你的工作进度。
- 格式化?
gofmt - 测试?
go test - 文档?
go doc - 依赖?
go mod
无聊,
但是好用。
6)云原生的绝对统治生态
Docker 是 Go 写的,K8s 是 Go 写的,Prometheus、Terraform、Etcd... 整个 CNCF(云原生计算基金会)的半壁江山都是 Go。当你需要操作容器、对接 gRPC、写 Kubernetes Operator、或者接入微服务网关时,Go 有第一公民级别的 SDK 支持。
你想做后台任务?想做指标?想做 tracing?想做限流?想做配置?想做 CLI?
Go 的库可能不一定最优雅,但几乎总有一个能用、能跑、能运维的方案。
不完美,但是足够满足需求
之前我说过,我没什么语言偏好,只是适合的业务选适合的语言。
选 Go,不是因为我们认为它是最完美的语言设计。
我们选它,是因为我们承认:我们不是在写诗,我们是在交付软件。
它丑,但它让你把注意力从语言表达力移到了业务逻辑上;它啰嗦,但它保证了你的服务跑在 1 核 2G 的轻量应用服务器上时,依然无畏并发;它的编译器不聪明,但它让你的构建流水线在几秒钟内完成。
所以,虽然我依然痛恨写 json:"id",依然厌恶满屏的 if err != nil,但当我要想要快速上线一个带后台任务、高并发、且需要长期稳定运行的 API 服务时……
这种舒适的体验,还得是 Go


