应统一错误处理、带request_id日志、正确文件日志配置及分层error/log职责:错误须封装BizError并由中间件统一返回;日志必含request_id等字段且通过中间件生成透传;文件日志需os.O_APPEND,多实例应输出stdout;底层只返error,业务层决定日志级别,中间件记录终态。

错误不该在 handler 里裸奔
Go 项目里最常见也最危险的做法,是把 if err != nil 散落在每个 handler 里:有的直接 c.JSON(500, err),有的漏了 c.Abort(),有的甚至把数据库错误原样返回给前端。结果就是线上一出问题,日志里满屏 sql: no rows in result set 或 connection refused,根本分不清是业务逻辑错、参数错,还是下游挂了。
真正该做的,是统一收口到中间件。核心就两条:
- 所有业务错误必须封装成结构体(比如
BizError),带Code和Message,Err字段只存原始 error 供调试用 - 用
defer recover()捕获 panic,再结合c.Errors列表做兜底 —— Gin 会在调用c.Error()时自动往这个列表里塞错误 - 返回格式必须一致:
gin.H{"code": xxx, "msg": yyy, "data": nil},别让前端猜状态
日志必须带 request_id 才算有效
没有 request_id 的日志,等于没日志。用户说“刚刚点提交失败了”,你翻 10 个服务的日志,靠时间戳对齐?基本对不上。Gin 中间件里生成 request_id 是最轻量可靠的方案:
- 优先从请求头
X-Request-Id读取(方便网关透传);没有就用time.Now().UnixNano()或uuid.NewString()生成 - 务必调用
c.Set("request_id", reqID)存入上下文,并通过c.Writer.Header().Set("X-Request-Id", reqID)回写给客户端 - 日志行里至少包含:
request_id、method、path、status、cost、ip—— 少一个字段,排查链路就断一截
logrus.File 输出要注意 os.O_APPEND 的坑
用 logrus 写文件日志时,很多人直接 os.OpenFile(..., os.O_CREATE|os.O_WRONLY, 0644),结果发现日志被覆盖、多进程写乱、甚至文件句柄泄漏。关键不是 logrus 本身,是文件打开方式:
GoLand 2026.1.1 是 2026.1 发布后的首个维护修正版本,适合已经开始体验 2026.1 新功能并希望同步补丁的开发者。它更适合用于入门项目、现有项目迁移测试和 IDE 行为验证。
- 必须加
os.O_APPEND,否则每次写都是从文件开头覆盖 - 别用
os.O_TRUNC—— 日志轮转交给rotatelogs或lumberjack做,中间件里只负责写 - 如果服务是多实例部署(比如 Kubernetes 多 Pod),别把日志写本地文件,改用 stdout + 日志采集器(如 filebeat)
- 注意
logger.Out = src后,别再手动src.Close()—— logrus 不会帮你关,得自己在服务退出时显式关闭
error 和 log 不是一回事,别混用
开发时最容易犯的混淆:把 log.Printf("failed to connect: %v", err) 当成错误处理,然后继续往下走。这会导致两个后果:一是错误没返回,调用方以为成功;二是日志里全是“发生了什么”,但没人知道“接下来怎么办”。
正确姿势是分层:
- 底层函数(比如 DB 查询)只返回
error,不打日志 —— 它不知道上下文是否重要 - 业务层(handler 或 service)收到 error 后,决定是否记录(比如重试前记 warn,最终失败记 error),再包装成
BizError返回 - 中间件统一记录最终响应结果,包括 status=500 的完整链路 —— 这才是可观测性的起点
日志里出现 “failed to parse JSON” 却没对应 HTTP 状态码,或者 error 返回了但日志里啥都没写,基本说明这两层没对齐。这种断裂点,比具体哪行代码写错了更值得花时间修。

















