Go 语言学习心得:真正重要的是工程习惯
wxk1991 Lv5

Go 语言学习心得:真正重要的是工程习惯

学 Go 的第一感觉通常很奇怪。

它不像 Java 那样到处都是设计模式,也不像 Python 那样写起来特别自由,更不像 JavaScript 那样生态变化很快。

Go 给人的感觉是:

1
语法不多,规则不复杂,但写好并不容易。

我刚开始学 Go 时,也觉得它“太简单了”。

变量、函数、结构体、接口、切片、map、goroutine,好像很快就能看完。

但真正写项目之后才发现:

1
Go 的难点不在语法,而在工程习惯。

一、先接受 Go 的朴素

Go 不太鼓励炫技。

很多代码看起来都很直白:

1
2
3
if err != nil {
return err
}

刚开始会觉得重复。

但写久了会发现,这种重复反而让代码很容易读。

你不需要猜异常从哪里冒出来,也不需要在一堆抽象里找真实逻辑。错误在哪里产生,就在哪里处理。

Go 的朴素不是缺点。

它是在提醒你:

1
把业务逻辑写清楚,比把代码写聪明更重要。

二、错误处理要认真写

Go 里没有传统意义上的 try/catch。

错误通常通过返回值传递:

1
2
3
4
user, err := repo.FindUser(ctx, id)
if err != nil {
return nil, err
}

这会逼你面对每一个失败分支。

数据库可能查不到。

HTTP 请求可能超时。

文件可能打不开。

JSON 可能解析失败。

这些都不是“边缘情况”,而是后端服务每天都会遇到的正常情况。

所以学 Go 时,不要急着嫌弃 if err != nil

更应该练的是:

1
2
3
这个错误应该在当前层处理,还是往上抛?
这个错误要不要包装上下文?
这个错误返回给用户时应该是什么状态码?

错误处理写清楚,Go 项目会非常稳。

错误处理随便写,Go 项目也会很乱。


三、Context 是后端链路的基础

学 Go 后端,一定要尽早理解 context.Context

它不是一个万能参数袋。

它真正重要的作用是:

1
传递取消信号、超时时间和请求级上下文。

一个 HTTP 请求进来之后,可能会调用数据库、远程接口、缓存、队列。

如果用户已经断开连接,或者请求已经超时,后面的操作就应该尽快停下来。

所以函数签名里经常会看到:

1
2
3
func GetUser(ctx context.Context, id int64) (*User, error) {
return nil, nil
}

这个 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
2
3
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
}

不要为了“看起来架构完整”,一开始就写一大堆接口。

Go 项目里最容易出现的问题之一,就是把简单业务写成“伪架构”:

1
controller -> service interface -> service impl -> manager -> dao interface -> dao impl

文件很多,但逻辑很少。

Go 更适合先把代码写清楚,等重复和边界真的出现,再抽象。


五、结构体要表达数据边界

Go 里经常会有几类结构体:

1
2
3
4
5
请求 DTO
响应 DTO
数据库模型
领域对象
配置对象

不要所有地方都复用同一个 struct。

比如数据库用户表可能有:

1
2
3
4
5
6
type User struct {
ID int64
Email string
PasswordHash string
CreatedAt time.Time
}

但接口返回不应该直接把它丢出去。

更合理的是:

1
2
3
4
type UserResponse struct {
ID int64 `json:"id"`
Email string `json:"email"`
}

这样做不是啰嗦。

这是在明确边界:

1
数据库里有什么,不等于接口应该返回什么。

Go 写久了会越来越重视这些边界。


六、并发不是上来就 goroutine

Go 的 goroutine 很好用。

但这也带来一个问题:

1
很多人太早使用并发。

看到循环就想开 goroutine,看到任务就想扔后台。

结果是:

  • 错误不好收集
  • context 没传好
  • 资源没有释放
  • 日志顺序混乱
  • 程序退出时任务还没结束

并发不是为了让代码看起来高级。

它应该解决明确的问题:

1
2
3
4
等待多个 IO
控制吞吐量
并行处理独立任务
避免阻塞主流程

写 goroutine 之前,先想清楚三件事:

1
2
3
谁负责取消?
谁负责等待?
错误怎么返回?

想不清楚,就先别并发。


七、包结构要为业务服务

Go 项目最常见的包结构争论很多。

我的理解是:

1
包结构不是模板题,而是维护题。

小项目可以很简单:

1
2
cmd/
internal/

再按业务拆:

1
2
3
4
internal/user
internal/order
internal/config
internal/database

不要为了“标准项目结构”复制一堆目录。

如果目录名不能帮助你找到代码,那这个目录就是噪音。

Go 的包名通常应该短、清楚、和职责一致。

比如:

1
2
3
4
5
user
auth
payment
config
logger

不要写太多抽象词:

1
2
3
4
5
common
utils
manager
handler
processor

这些名字不是不能用,而是很容易变成什么都能塞的筐。


八、测试要从纯逻辑开始

Go 的测试工具很朴素,但很好用。

最常见的是 table-driven tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func TestNormalizeEmail(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"trim and lower", " [email protected] ", "[email protected]"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NormalizeEmail(tt.in)
if got != tt.want {
t.Fatalf("got %q, want %q", got, tt.want)
}
})
}
}

刚开始不要急着测整个 HTTP 服务。

可以先测这些东西:

  • 参数校验
  • 字符串处理
  • 时间计算
  • 权限判断
  • 状态流转
  • SQL 构造前的业务逻辑

纯逻辑测试写顺了,再去写 handler 测试、repository 测试、集成测试。


九、Go 写得好不好,看删除代码时痛不痛

我判断一个 Go 项目写得好不好,有一个很简单的标准:

1
删除一个功能时,会不会牵一堆无关文件。

如果一个小功能散落在十几个“通用层”里,说明抽象可能太早了。

如果一个包依赖了很多不相关的包,说明边界可能没守住。

如果一个结构体在请求、响应、数据库、缓存里到处复用,说明数据边界可能太模糊。

Go 代码最舒服的状态是:

1
读得懂,改得动,删得掉。

这比“看起来很高级”重要得多。


十、结语

学 Go,不要只盯着语法。

语法很快就能过一遍。

真正要反复练的是:

1
2
3
4
5
6
7
错误处理
context 传递
接口边界
结构体设计
包结构
并发收敛
测试习惯

Go 的魅力在于,它不会用复杂语法替你遮住工程问题。

你怎么设计边界,怎么处理错误,怎么组织代码,都会很直接地体现在项目里。

这也是我越来越喜欢 Go 的原因:

1
它逼你把事情想清楚,然后用朴素的代码写出来。