Go 语言学习心得:真正重要的是工程习惯
学 Go 的第一感觉通常很奇怪。
它不像 Java 那样到处都是设计模式,也不像 Python 那样写起来特别自由,更不像 JavaScript 那样生态变化很快。
Go 给人的感觉是:
1 | 语法不多,规则不复杂,但写好并不容易。 |
我刚开始学 Go 时,也觉得它“太简单了”。
变量、函数、结构体、接口、切片、map、goroutine,好像很快就能看完。
但真正写项目之后才发现:
1 | Go 的难点不在语法,而在工程习惯。 |
一、先接受 Go 的朴素
Go 不太鼓励炫技。
很多代码看起来都很直白:
1 | if err != nil { |
刚开始会觉得重复。
但写久了会发现,这种重复反而让代码很容易读。
你不需要猜异常从哪里冒出来,也不需要在一堆抽象里找真实逻辑。错误在哪里产生,就在哪里处理。
Go 的朴素不是缺点。
它是在提醒你:
1 | 把业务逻辑写清楚,比把代码写聪明更重要。 |
二、错误处理要认真写
Go 里没有传统意义上的 try/catch。
错误通常通过返回值传递:
1 | user, err := repo.FindUser(ctx, id) |
这会逼你面对每一个失败分支。
数据库可能查不到。
HTTP 请求可能超时。
文件可能打不开。
JSON 可能解析失败。
这些都不是“边缘情况”,而是后端服务每天都会遇到的正常情况。
所以学 Go 时,不要急着嫌弃 if err != nil。
更应该练的是:
1 | 这个错误应该在当前层处理,还是往上抛? |
错误处理写清楚,Go 项目会非常稳。
错误处理随便写,Go 项目也会很乱。
三、Context 是后端链路的基础
学 Go 后端,一定要尽早理解 context.Context。
它不是一个万能参数袋。
它真正重要的作用是:
1 | 传递取消信号、超时时间和请求级上下文。 |
一个 HTTP 请求进来之后,可能会调用数据库、远程接口、缓存、队列。
如果用户已经断开连接,或者请求已经超时,后面的操作就应该尽快停下来。
所以函数签名里经常会看到:
1 | func GetUser(ctx context.Context, id int64) (*User, error) { |
这个 ctx 应该一路传下去。
数据库查询用:
1 | db.QueryRowContext(ctx, sql, id) |
HTTP 请求用:
1 | http.NewRequestWithContext(ctx, http.MethodGet, url, nil) |
学会传 context,才算真正开始写 Go 后端。
四、接口不要乱抽象
Go 的接口很轻。
它不是必须先定义再实现。
更常见的方式是:
1 | 谁使用,谁定义接口。 |
比如 service 依赖 user repository,就在 service 附近定义它真正需要的方法:
1 | type UserRepository interface { |
不要为了“看起来架构完整”,一开始就写一大堆接口。
Go 项目里最容易出现的问题之一,就是把简单业务写成“伪架构”:
1 | controller -> service interface -> service impl -> manager -> dao interface -> dao impl |
文件很多,但逻辑很少。
Go 更适合先把代码写清楚,等重复和边界真的出现,再抽象。
五、结构体要表达数据边界
Go 里经常会有几类结构体:
1 | 请求 DTO |
不要所有地方都复用同一个 struct。
比如数据库用户表可能有:
1 | type User struct { |
但接口返回不应该直接把它丢出去。
更合理的是:
1 | type UserResponse struct { |
这样做不是啰嗦。
这是在明确边界:
1 | 数据库里有什么,不等于接口应该返回什么。 |
Go 写久了会越来越重视这些边界。
六、并发不是上来就 goroutine
Go 的 goroutine 很好用。
但这也带来一个问题:
1 | 很多人太早使用并发。 |
看到循环就想开 goroutine,看到任务就想扔后台。
结果是:
- 错误不好收集
- context 没传好
- 资源没有释放
- 日志顺序混乱
- 程序退出时任务还没结束
并发不是为了让代码看起来高级。
它应该解决明确的问题:
1 | 等待多个 IO |
写 goroutine 之前,先想清楚三件事:
1 | 谁负责取消? |
想不清楚,就先别并发。
七、包结构要为业务服务
Go 项目最常见的包结构争论很多。
我的理解是:
1 | 包结构不是模板题,而是维护题。 |
小项目可以很简单:
1 | cmd/ |
再按业务拆:
1 | internal/user |
不要为了“标准项目结构”复制一堆目录。
如果目录名不能帮助你找到代码,那这个目录就是噪音。
Go 的包名通常应该短、清楚、和职责一致。
比如:
1 | user |
不要写太多抽象词:
1 | common |
这些名字不是不能用,而是很容易变成什么都能塞的筐。
八、测试要从纯逻辑开始
Go 的测试工具很朴素,但很好用。
最常见的是 table-driven tests:
1 | func TestNormalizeEmail(t *testing.T) { |
刚开始不要急着测整个 HTTP 服务。
可以先测这些东西:
- 参数校验
- 字符串处理
- 时间计算
- 权限判断
- 状态流转
- SQL 构造前的业务逻辑
纯逻辑测试写顺了,再去写 handler 测试、repository 测试、集成测试。
九、Go 写得好不好,看删除代码时痛不痛
我判断一个 Go 项目写得好不好,有一个很简单的标准:
1 | 删除一个功能时,会不会牵一堆无关文件。 |
如果一个小功能散落在十几个“通用层”里,说明抽象可能太早了。
如果一个包依赖了很多不相关的包,说明边界可能没守住。
如果一个结构体在请求、响应、数据库、缓存里到处复用,说明数据边界可能太模糊。
Go 代码最舒服的状态是:
1 | 读得懂,改得动,删得掉。 |
这比“看起来很高级”重要得多。
十、结语
学 Go,不要只盯着语法。
语法很快就能过一遍。
真正要反复练的是:
1 | 错误处理 |
Go 的魅力在于,它不会用复杂语法替你遮住工程问题。
你怎么设计边界,怎么处理错误,怎么组织代码,都会很直接地体现在项目里。
这也是我越来越喜欢 Go 的原因:
1 | 它逼你把事情想清楚,然后用朴素的代码写出来。 |