Go 语言初体验:Less is more,一种丑但可靠的工程美学

grtsinry43
1/26/2026
18 views
预计阅读时长 13 分钟

AI Summary

DEEPSEEK-R1

最近新项目评估技术栈,因为 Java/Kotlin 太重,TypeScript/JavaScript(Node.js)因为 js 原型链的问题我一直感觉不是合格的后端语言,写 Rust 的话社区根本不会有几个人贡献,再加上后台任务,轻量化,易于部署,可能就只有 Go 能担任这个职责了。
其实我之前用过 go 的,当时在搞 AI 原型生成器的时候,为了快捷操作容器,我用 go 搞了个沙箱管理器,操作容器,对外 gRPC,利用了它在云原生领域的生态优势。而这次,我看中的是它易于入门,编译快,占用轻,易于部署,当然重要的是,协程模型确实很现代很舒服。

真的很丑

我对 Go 的第一印象非常稳定:丑。
对于习惯了 Java/JS 的注解(装饰器),Kotlin 的 DSL,Rust 的宏来说的我,Go 的语法极其贫瘠,真是可以说简陋。对于 Go 的显式哲学来说,不像是语言的搭积木,而是你把螺丝刀给我,我就能把家装起来,但别问我为什么这个螺丝长这样。

于是字符串成了注解

我们从这样一段例子开始:

go
1// OAuthProviderResp 返回可用的 OAuth provider 信息。
2type OAuthProviderResp struct {
3    Key          string   `json:"key"`
4    DisplayName  string   `json:"displayName"`
5    Scopes       []string `json:"scopes"`
6    PKCERequired bool     `json:"pkceRequired"`
7}
8
9type Webhook struct {
10    ID              int64          `gorm:"column:id;primaryKey"`

为了做一个简单的序列化和参数校验,必须在结构体后面跟上一长串 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 里,你得处理 Promiseasync/await 传染性,一旦忘了 await 就像踩了雷;在 Rust 里,你得面对 Tokio 的运行时选择、PinFuture 的生命周期……心智负担极重。

而 Go 的 Goroutine 是对开发者最友好的并发模型,没有之一:

go
1// 无论这个任务多复杂,哪怕它是 IO 密集型
2go func() {
3    processBackgroundJob(data)
4}()

就这一行,Go 运行时帮你解决了 M

的调度,帮你处理了上下文切换。你写的是线性的、符合直觉的同步代码,底层跑的确是高效的异步非阻塞逻辑。

所以为什么我和朋友总会相互开玩笑,说 Go 工程师想的都是只要业务写完了,剩下的就爽了。让你面对真实需求的时候,Go 的 selectchannel 让你能像搭积木一样优雅地控制并发,而不是陷入回调地狱或生命周期深渊。

3)“丑”的另一面,是可维护

说实话,Go 语法上的丑,很大一部分其实是 Go 的一种强行约束:别太聪明,他让代码逻辑绝对平铺,显式写出了一切。

  • 没有宏 → 你没法把业务塞进编译期魔法里,接手的人能轻松读懂。
  • 没有注解 → 你必须显式声明逻辑,代码更有可读性。
  • 错误处理啰嗦 → 你很难忘记处理,也很难忽略掉业务里每一步的问题。

你可以随便招一个开发者,让他看两天文档,他写出来的代码虽然丑,但你一眼就能看懂他在干嘛。Review 代码不再需要脑补上下文和复杂的继承关系,所见即所得。

Go 的哲学在于,它强迫所有人都用最笨的方式写代码,从而消灭了奇技淫巧带来的维护成本。这在个人项目里可能不突出,但在多人协作和长期演进里,便让可维护性到了其他语言无法企及的地步。

4)交叉编译,DevOps 的终极梦想

这部分甚至无需多言。

bash
1CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server main.go

回车敲下,你会得到一个纯静态链接的二进制文件。 没有 node_modules 黑洞,没有 JVM 依赖,没有 glibc 版本冲突。 你把这个文件 scp 到服务器上,chmod +x,然后 ./server,就这么简单。

配合 Docker,你的 Dockerfile 可能只有 5 行:

Dockerfile
1FROM alpine
2COPY --from=builder /app/server /server
3CMD ["/server"]

这对于我们这种“个人开发者”或“小团队”来说,省下的时间就是生命。

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

相关推荐

手把手带你玩转 Monorepo,拥抱现代前端开发新范式

> 你是否曾被这些问题困扰? > > * 管理多个相互关联的 Git 仓库(或是复杂的 git ...

grtsinry43
7/21/2025
8225
1
10
现代安卓开发之 Jetpack Compose、Xposed Hook 与 Kotlin Multiplatform

现代安卓开发之 Jetpack Compose、Xposed Hook 与 Kotlin Multiplatform

emm上来堆三个技术名词太劝退,每个部分开始之前我都会写一段简短的介绍,用很通俗的语言讲解下这是个什...

grtsinry43
4/23/2025
1006
0
4
2025 年终总结——从晨光到雾散,化经历为成长

2025 年终总结——从晨光到雾散,化经历为成长

> 副标题:在爱中重新振作,于是我们真的曾将彼此照亮 说实话,我是一个很喜欢总结的人,而真的到想要...

grtsinry43
12/22/2025
713
0
12

折腾记录|使用 Nuxt.js 重写个人主页,使用 SSR 优化 SEO ,实现一些期待已久的效果

在 22 年刚创建个人主页的时候,由于我的技术水平不够,只能用一些 wordpress type...

grtsinry43
9/19/2024
406
0
2
新时代的 PHP:RSC 的边界错位与工程代价

新时代的 PHP:RSC 的边界错位与工程代价

讽刺的是,这篇文章,也就是你看到这个网站,正在运行在 Next.js 上,但由于无法承受的问题和维护...

grtsinry43
12/14/2025
241
0
7

发表评论

在这里畅所欲言吧!
支持 Markdown 语法0 / 3000

网站运行时间

0
0
0
0

在风雨飘摇之中

感谢陪伴与支持

愿我们不负热爱,继续前行