第五章 奠定基石 —— 项目的“七通一平”工程
文章目录
你之前的代码感觉奇怪,这是非常敏锐的直觉。
作为一名高级开发专家,我认为“奇怪”的核心点在于:你的 Error 结构体和 response 包的职责耦合了,但又没有完全耦合好。
errcode.Error结构体中有一个details字段,但response.Fail函数却忽略了它。- 这迫使你创建了一个
response.FailWithDetails函数,它再次忽略了err.details,而是要求你传入一个新的details参数。 - 这导致调用者必须“记住”使用哪个
Fail函数,并且errcode.Error对象本身携带的信息(details)被浪费了。
一个更高级、更健壮的设计遵循以下原则:
- 错误即数据(Error as Data):
errcode.Error对象应该是一个不可变的数据载体。它应该能通过链式方法(如WithDetails)创建新的副本来携带所有上下文(自定义消息、详细数据等)。 - 响应即渲染(Response as Render):
response包的职责应该极其单一:它只负责将error或data渲染为 HTTP 响应。 - 单一失败处理(Single Fail Handler): 只应该有一个
response.Fail(err error)函数。它应该足够“聪明”,能够处理任何error:- 如果
err是我们自定义的ecode.Error,它就按ecode.Error中的Code,Message,Details和StatusCode来渲染。 - 如果
err是一个未知的error(例如sql.ErrNoRows),它应该自动将其渲染为ServerError(500) 并隐藏内部错误细节(但应该在服务端打印日志)。
- 如果
基于这个理念,这是我的重构方案。
🚀 重构方案:职责分离与链式错误
我将包名 errcode 改为 ecode(Error Code,更简洁)。
1. pkg/ecode/error.go (错误定义)
这是变化最大的地方。我们移除全局的 codes map 和 panic,因为错误码重复是开发期问题,应该由测试或 Linter 解决,而不是在运行时 panic。
我们添加了 Is() 方法以兼容标准库 errors.Is,并提供了强大的 WithMessage 和 WithDetails 链式方法。
|
|
2. pkg/ecode/code.go (错误码常量)
这个文件几乎不变,只是调用了新的 NewError 构造函数。
|
|
3. pkg/response/response.go (HTTP 响应层)
这是最能体现新设计优势的地方。Fail 函数现在是唯一的错误出口,并且变得极其强大。
|
|
✨ 新的调用方式(天壤之别)
现在你的 Handler 层代码会变得极其干净。你不再需要 FailWithMsg 或 FailWithDetails。
场景1:登录成功,只需提示
- 优化前:
response.SuccessWithMsg(ctx, gin.H{}, "登录成功") - 优化后:
response.SuccessMessage(ctx, "登录成功")
场景2:获取用户信息成功
- 优化前:
response.Success(ctx, user) - 优化后:
response.Success(ctx, user)
场景3:参数错误(标准消息)
- Service 层:
return ecode.InvalidParams - Handler 层:
response.Fail(ctx, err) - JSON 响应:
{"code": 10000001, "message": "入参错误"}
场景4:参数错误(自定义消息)
- Service 层:
return ecode.InvalidParams.WithMessage("用户名不能为空") - Handler 层:
response.Fail(ctx, err) - JSON 响应:
{"code": 10000001, "message": "用户名不能为空"}
场景5:参数错误(带详细数据,如表单验证失败)
- Service 层:
1 2 3 4 5details := map[string]string{ "username": "用户名不能为空", "password": "密码长度不能少于8位", } return ecode.InvalidParams.WithDetails(details) - Handler 层:
response.Fail(ctx, err) - JSON 响应:
1 2 3 4 5 6 7 8{ "code": 10000001, "message": "入参错误", "data": { "username": "用户名不能为空", "password": "密码长度不能少于8位" } }
场景6:数据库查询为空(未知错误)
- Service 层:
return sql.ErrNoRows - Handler 层:
response.Fail(ctx, err) - JSON 响应:
{"code": 10000000, "message": "服务内部错误"}(同时服务端会打印sql.ErrNoRows的日志)
总结
这个设计的好处是:
- 高度解耦:
ecode只负责定义错误数据,response只负责渲染。 - 调用简单:
Handler层永远只有一个response.Fail(ctx, err),它不关心err是什么类型、是否带Details。 - 功能强大:
Service层可以通过链式调用WithMessage或WithDetails自由地为错误附加任何上下文。 - 安全健壮: 自动隐藏未知错误,防止内部实现细节泄露。