这篇不是系统教程,只是整理几个 Go 项目里经常会用到的小习惯。
这些点都不复杂,但如果一开始没想清楚,项目变大之后会很难受。
我以前觉得 Go 项目的“工程味”主要靠目录结构,后来发现更常出问题的是这些细节:资源释放放在哪、错误在哪一层补上下文、日志谁来打、context 到底传什么。它们单独看都很小,叠起来就是项目维护成本。
defer 不只是少写一行 Close
defer 最常见的用法是释放资源:
这当然没问题,但有两个细节我后来比较注意。
第一个是循环里不要随手 defer。比如处理很多文件时,如果在循环里 defer close,文件句柄会等函数返回才释放,不是每次循环结束就释放。
把单次处理拆到小函数里,反而更清楚。
第二个是 defer 里如果有错误,不要完全忽略。比如写文件时,Close 也可能返回错误。日志可以记,或者在需要时合并返回。
写文件时我会更谨慎一点,因为有些错误可能在 flush/close 时才暴露:
这个写法不是每个地方都需要,但在写配置、写导出文件、写临时产物时,我会比读文件更认真处理 Close。
context 只放请求生命周期相关的东西
context.Context 很好用,但也很容易被当成万能参数袋。
我现在比较坚持一个边界:context 里放取消信号、超时、trace id、request id 这类请求生命周期相关信息,不放业务参数。
不太推荐这样:
| |
如果 userID 是业务逻辑必须显式依赖的东西,直接传参数更清楚:
context 的价值是把取消、超时、跨调用链信息传下去,不是把函数签名藏起来。
错误处理要留住上下文
Go 里错误处理最容易写成这样:
小函数里问题不大,但跨几层调用之后,最终日志只看到 connection refused,就不知道是哪一步失败了。
我更习惯在边界位置包一层上下文:
然后在上层需要判断错误类型时,用 errors.Is 或 errors.As:
这样既能保留原始错误,又能让日志里有足够线索。
日志边界不要太散
日志不是越多越好。很多项目的问题不是没日志,而是日志到处打,真正出问题时看不出主线。
我的习惯是:
- 入口层记录请求开始、结束、耗时和 trace id。
- 业务层只记录关键状态变化。
- 基础设施层返回带上下文的错误,不随便打 error 日志。
- 最外层统一把错误打出来。
否则一个错误从 DAO 层、service 层、handler 层各打一遍,日志系统里看起来像出了三个问题。
Go 1.21 之后标准库里有 log/slog,结构化日志更方便了。即使不用 slog,也建议日志字段固定下来,比如 request_id、user_id、method、path、latency_ms。
我会尽量避免这种日志:
| |
它的问题不是不能看,而是上下文太少。更好的方式是把动作、对象和链路字段带上:
日志的边界想清楚以后,错误包装和日志字段会互相配合,而不是每一层都打一遍。
小结
这些技巧都不算高级,但很影响项目长期维护:
defer适合资源释放,但注意循环和 Close 错误。context传生命周期,不传业务参数。- 错误返回要带动作上下文。
- 日志尽量在边界统一记录,不要每层都打一遍。
Go 的代码很多时候不是写不出来,而是写久了之后还能不能看懂。这个区别挺大。