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

原文：https://blog.grtsinry43.com/posts/go-first-experience/

作者：grtsinry43

发布时间：2026-01-26T05:47:33Z

内容更新时间：2026-01-26T05:47:33Z

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

## 真的很丑

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

### 于是字符串成了注解

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

```go
// OAuthProviderResp 返回可用的 OAuth provider 信息。
type OAuthProviderResp struct {
    Key          string   `json:"key"`
    DisplayName  string   `json:"displayName"`
    Scopes       []string `json:"scopes"`
    PKCERequired bool     `json:"pkceRequired"`
}

type Webhook struct {
    ID              int64          `gorm:"column:id;primaryKey"`
    Name            string         `gorm:"column:name;size:100;not null"`
    URL             string         `gorm:"column:url;size:512;not null"`
    Events          []byte         `gorm:"column:events;type:jsonb;not null"`
    Headers         []byte         `gorm:"column:headers;type:jsonb;not null"`
    PayloadTemplate string         `gorm:"column:payload_template;type:text;not null"`
    IsEnabled       bool           `gorm:"column:is_enabled"`
    CreatedAt       time.Time      `gorm:"column:created_at;autoCreateTime"`
    UpdatedAt       time.Time      `gorm:"column:updated_at;autoUpdateTime"`
    DeletedAt       gorm.DeletedAt `gorm:"column:deleted_at;index"`
}
```

为了做一个简单的序列化和参数校验，必须在结构体后面跟上一长串 `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
// 无论这个任务多复杂，哪怕它是 IO 密集型
go func() {
    processBackgroundJob(data)
}()
```

就这一行，Go 运行时帮你解决了 M:N 的调度，帮你处理了上下文切换。你写的是线性的、符合直觉的同步代码，底层跑的确是高效的异步非阻塞逻辑。

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

### 3）“丑”的另一面，是可维护

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

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

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

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

### 4）交叉编译，DevOps 的终极梦想

这部分甚至无需多言。

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

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

配合 Docker，你的 Dockerfile 可能只有 5 行：

```Dockerfile
FROM alpine
COPY --from=builder /app/server /server
CMD ["/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
