上文你提到了"报错即链条",能不能清楚描述一下什么意思,Go的return err和Java/PHP的try-catch为什么要搞这么大差别?
这个弯儿确实得绕一下。Java/PHP 的异常是"横冲直撞"的,而 Go 的错误是"接力棒"。
我用一个"楼下保安抓小偷"的故事,一次性给你讲透"包装"和"链条":
1. 场景:数据库连接断了(小偷进屋了)
第一步:Repo 层(发现异常的现场保安)
保安发现门锁被撬了,但他不知道小偷想干嘛(是偷钱还是偷资料?)。他不能自己在那喊,他得汇报。
-
做法:把现场情况"包装"一下,往上传。
// internal/repo/user.go
func (r *UserRepo) GetByID(id int) (*User, error) {
err := r.pool.QueryRow(...)
if err != nil {
// 【错误包装】:%w 就像是在证据袋里装了原件,外面贴了张纸条说明
return nil, fmt.Errorf("数据库底层报错 (ID:%d): %w", id, err)
}
return user, nil
}
第二步:Logic 层(保安队长)
队长接到消息,他知道这个 ID 是"大客户"。他需要增加业务信息,继续往上传。
-
做法:不打日志,继续包装。
// internal/logic/user.go
func (l *UserLogic) UserProfile(id int) (*User, error) {
user, err := l.repo.GetByID(id)
if err != nil {
// 【再次包装】:叠罗汉,加上业务层的描述
return nil, fmt.Errorf("查询大客户资料失败: %w", err)
}
return user, nil
}
第三步:Middleware/Handler(总指挥中心)
终于到了处理问题的地方了。这里是链条的终点。
-
做法:记录日志,并告诉前端(报信)。
// internal/handler/user.go
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
user, err := logic.UserProfile(123)
if err != nil {
// 【终结错误】:在这里,我们才真正调用 infra.Log
// 因为 err 里包含了刚才每一层包装的信息,打印出来长这样:
// "查询大客户资料失败: 数据库底层报错 (ID:123): connection refused"
infra.Log("api").Error("请求失败", "err", err.Error())res.ApiError(w, 500, 10001, "系统忙") // 给用户的只有一句话 return } res.ApiSuccess(w, user)}
2. 为什么这样是"最优解"?
| 概念 | 形象比喻 | 实际作用 |
|---|---|---|
| 错误包装 (%w) | 叠罗汉 | 让错误在每一层都带上"当时在干嘛"的信息,而不是只有一个干巴巴的 connection refused。 |
| 报错即链条 | 接力棒 | 保证一个错误只在终点打一次日志。避免你的日志文件里,同一个错误在 Repo 出现一次、Logic 出现一次、Handler 又出现一次。 |
3. "包装"的终极威力
当你在日志里看到一串:
查询大客户资料失败: 数据库底层报错 (ID:123): connection refused
你会瞬间明白:
- 最底层:网络不通(connection refused)。
- 中间层:是在查 ID 为 123 的时候崩的。
- 业务层:这是大客户模块的问题,得赶紧修!
4. 总结你的疑惑
- 问:日志那里不是白写了?
- 答:没白写!日志函数 (
infra.Log) 是"笔",包装 (%w) 是"墨"。你以前是在每张纸上写一个字(到处打日志),现在是把所有信息聚成一句话,最后在这张纸上写个大的(只在终点打一次)。
- 答:没白写!日志函数 (
- 问:我要再写一次这个?
- 答:不需要。你只需要在 Repo 和 Logic 里写
fmt.Errorf,只在 Middleware 或 Handler 里写一次Log().Error()。
- 答:不需要。你只需要在 Repo 和 Logic 里写