Go 后端编程小书(上) · 卷 1

第 12 章:写 Go 后端的几个长期习惯

本章问题:学完上册后,哪些习惯比记住更多语法更重要?


简单不是少写几行

Go 经常被说成简单语言。但这里的"简单"不是指代码一定最短。

显式错误处理会多写几行。结构体字段要大写才能导出,JSON tag 又要写一遍。没有异常,没有继承,没有太多语法糖。刚开始看,这些都不像"简单"。

Go 的简单更接近另一件事:读代码时,你不需要猜太多。

函数返回了 error,你知道它可能失败。包里首字母大写的名字,你知道外部可以访问。handler 里写了状态码,你知道接口如何响应。context 一路传下去,你知道请求取消有机会传到底层。

后端服务会长期运行,也会长期被修改。能被读懂,比一时写得短更重要。


错误路径要可见

Go 后端代码里,最值得保留的习惯是认真处理错误。

不要忽略错误:

go
data, _ := os.ReadFile(path)

除非你非常确定错误无关紧要,否则不要这样写。

也不要在底层到处 log.Fatal。底层函数应该返回错误,让上层决定如何处理。入口层、HTTP handler、后台任务边界,才是更适合记录和翻译错误的地方。

一个好的错误链条应该能回答:哪一步失败?处理的对象是什么?底层错误是什么?对用户应该返回什么?

这比"优雅"的异常包装更实用。


接口要小,出现要晚

Go 的 interface 很轻,所以也很容易被滥用。

第四章我们提取了 EmailSender,因为 WelcomeUser 确实需要替换发送器来测试。这个接口有理由存在。但如果一开始就为每个类型都写一个接口,你只是在制造名字。

先写具体代码,再在摩擦出现时提取接口。接口越小,越有用。一个只有 Send(...) error 的接口,往往比一个包含十几个方法的 UserService 更容易维护。


其他值得保持的习惯

前面几章还建立了一些习惯,这里做一个简短回顾:

测试先覆盖稳定规则。 优先测试参数校验、配置加载、错误分支和 HTTP 状态码。不要一开始就追求复杂 mock。测试的目标是让你敢改代码。

context 是后端函数的边界信号。 看到函数需要访问数据库、调用外部接口或执行耗时任务时,让它接收 context.Context 作为第一个参数。这表示它属于某个请求或任务的生命周期。

main 函数是服务的装配点。 main 不应该塞满业务逻辑,它更像装配点:

go
func main() {    cfg, err := config.Load("config.json")    if err != nil {        log.Fatal(err)    }    // 初始化依赖    sender := user.ConsoleEmailSender{}    // 注册路由    mux := http.NewServeMux()    mux.HandleFunc("/healthz", healthHandler)    mux.HandleFunc("/users", userHandler)    // 创建 server    server := &http.Server{        Addr:              ":" + cfg.Port,        Handler:           mux,        ReadHeaderTimeout: 5 * time.Second,    }    // 启动服务、处理退出    // ...}

这样做的好处是,业务函数可以脱离进程启动逻辑单独测试。你不需要启动整个服务,才能测试一个用户校验函数。


上册到这里完成了什么

如果你跟着上册敲过代码,现在回头看,你已经走过了一段不短的路。

你从一个 go mod init 开始,写了第一个 main.go,学会了用 struct 表达业务数据、用 method 组织行为、用 interface 隔离依赖。然后你给代码加上了显式的错误处理,把堆积在一个文件里的代码拆成了多个包,写了一个能从 JSON 文件和环境变量读取配置的配置加载器。接着你用标准库写了一个完整的 HTTP 服务——有路由、有 JSON 响应、有状态码,并给它写了测试。你学了 goroutine 和 channel 来处理并发,用 context 控制超时和取消,最后把整个程序构建成了一个可以部署的二进制,加上了日志、超时和优雅退出。

这不是"了解了一下 Go"。这是一个小型后端服务的完整骨架。它还没有连接数据库,但它已经知道怎么启动、怎么响应请求、怎么处理失败、怎么安全退出。


接下来做什么

上册的服务还缺一些东西。

它没有连接数据库——用户数据还硬编码在 handler 里,每次重启就丢了。中册会从连接一个真实的数据库开始,学习怎么用 Go 做数据持久化。

它没有认证和授权——任何人都能调用创建用户的接口。中册会加上 token 验证和权限检查。

它没有缓存——每次请求都重新计算。它没有消息队列——发邮件是同步阻塞的,高峰期会拖慢响应。它也没有指标和追踪——出了问题你不知道慢在哪里。

这些能力会在中册和下册里,放到更真实的工程场景中展开:连接数据库、设计分层 API、做认证、拆分包边界、接入缓存和消息队列、加入日志指标和追踪。

上册给你的是地基。后面的楼层会在这个地基上一层层搭上去。


最后一条建议

写 Go 时,不要急着证明自己会很多抽象。

先让代码能跑,再让失败路径清楚,再让边界变小,再让测试补上。很多时候,这比一开始设计一个漂亮架构更可靠。Go 社区里很多成熟的开源项目——Docker、Kubernetes、Prometheus——代码风格都出奇地朴素。它们不是不会写得花哨,而是选择了不花哨。

这种选择背后有一个判断:后端服务的寿命通常比写它的时间长得多。你今天写的代码,半年后会有另一个人(或者半年后的你自己)来读、来改、来排查问题。那时候,清楚比聪明有用得多。

Go 的价值,也常常就在这里:它把你拉回到程序最基本的事情上——让数据有形状,让失败可见,让并发可控,让服务能跑起来,也能安全停下来。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus