GoFrame 优化接口的错误码和异常的思路
前言
你是否想在使用 GoFrame 的过程中,拥有一个能打印异常堆栈,能自定义响应状态码,能统一处理响应数据的接口。如果你回答是,那么,请耐心看完本文,或许会对你有所启发。若文中由表达不当之处,恳请不吝赐教。
异常都需要错误堆栈吗
为什么会问这个问题呢,所有的接口错误都会向日志中抛出堆栈信息,这不是好事吗?答案是否定的。
业务开发中,通常有业务异常和系统异常两种 err,我这里暂且这么称呼,也有的称为业务异常为"错误",系统异常为"异常"。业务异常是由用户输入不当引起的,比如说账号密码错误,这种 err 通常只返回给用户即可,不需要打印堆栈信息。而系统异常是由系统内部自发引起的,比如说 SQL 语句不当,这种错误需要打印堆栈信息,且不能把 err 返回到用户那里,不然会暴露代码结构,严重的可能会暴露数据库结构。
在 GoFrame 中,因为有着强大的 gerror 组件,所以只要接收了任何组件方法中的 err,不论业务异常和系统异常,都会打印堆栈信息,这与我们的设计目标不符合,需要解决它。
状态码
此处的状态码区别与 HTTP 状态码,它是我们自定义的一套业务码,比如这样:
{
	"code": 10001,
	"message": "用户名密码错误",
	"data": null
}
{
	"code": 10002,
	"message": "用户不存在",
	"data": null
}
它们的 HTTP 状态码都是 200,代表响应成功,但是业务状态码不同,用以区分不同的业务异常。
一个例子
我们来编写一个完整的示例:
接口文件:/api/exception/v1/exception.go:
// 模拟业务异常
type BusinessReq struct {
	g.Meta `path:"/business" method:"get"`
}
type BusinessRes struct {
	Name string
	Age  int
}
// 模拟系统异常
type SystemReq struct {
	g.Meta `path:"/system" method:"get"`
}
type SystemRes struct {
	Name string
	Age  int
}
控制器文件:/internal/controller/exception/exception_v1_*.go:
func (c *ControllerV1) Business(ctx context.Context, req *v1.BusinessReq) (res *v1.BusinessRes, err error) {
	err = service.Exception().Business()
	if err != nil {
		return nil, err
	}
	return &v1.BusinessRes{
		Name: "business",
		Age:  1,
	}, nil
}
func (c *ControllerV1) System(ctx context.Context, req *v1.SystemReq) (res *v1.SystemRes, err error) {
	err = service.Exception().System()
	if err != nil {
		return nil, err
	}
	return &v1.SystemRes{
		Name: "system",
		Age:  1,
	}, nil
}
服务文件:/internal/logic/exception/exception.go:
func (s *sException) Business() error {
	return gerror.New("用户名密码错误")
}
// System 这里我们对 gjson.Decode() 传入错误数据,用来模拟组件内部抛出err
func (s *sException) System() error {
	_, err := gjson.Decode("")
	if err != nil {
		return err
	}
	return nil
}
这个例子模拟了一个完整的接口,从 api 到 controller 到 logic,然后我们请求它们,分别从响应信息和控制台两个角度看看它们的结果。
业务异常 Business
curl http://127.0.0.1:8000/business
控制台:

接口响应:
{
	"code": 50,
	"message": "用户名密码错误",
	"data": null
}
系统异常 System
curl http://127.0.0.1:8000/system
控制台:

接口响应:
{
	"code": 50,
	"message": "json Decode failed: EOF",
	"data": null
}
优化方案
此时,我们的接口中有三个不足:
- 业务异常不应该抛出堆栈,因为用户名或密码错误的堆栈没有意义;
 - 系统异常的响应信息中, 
message不应该抛出 "json Decode failed: EOF",应该使用未知错误或者系统错误这类字眼; - 业务异常和系统异常的业务码,也就是响应信息中的 
code,不应该都使用 50,应当做以区分。 
设计统一 err
在 GoFrame 的工程目录中,有一个包 /internal/packed,我们可以在此处编写我们自己的 err 处理,后面的代码可以做为参考,也可以直接复制过去用:
/internal/packed/err.go:
type pErr struct {
	maps map[int]string
}
var Err = &pErr{
	maps: map[int]string{
		0:     "请求成功",
		10001: "用户名或密码错误",
		10002: "用户不存在",
		99999: "未知错误",
	},
}
// GetMsg 获取code码对应的msg
func (c *pErr) GetMsg(code int) string {
	return c.maps[code]
}
// Skip 抛出一个业务级别的错误,不会打印错误堆栈信息
func (c *pErr) Skip(code int, msg ...string) (err error) {
	var msgStr string
	if len(msg) == 0 {
		msgStr = c.GetMsg(code)
	} else {
		msg = append([]string{c.GetMsg(code)}, msg...)
		msgStr = strings.Join(msg, ", ")
	}
	// NewWithOption 在低版本的 gf 上不存在,请改用 NewOption
	return gerror.NewWithOption(gerror.Option{
		Stack: false,
		Text:  msgStr,
		Code:  gcode.New(code, "", nil),
	})
}
// Sys 抛出一个系统级别的错误,使用code码:99999,会打印错误堆栈信息
// msg 接受string和error类型
// !!! 使用该方法传入error类型时,一定要注意不要泄露系统信息
func (c *pErr) Sys(msg ...interface{}) error {
	var (
		code     = 99999
		msgSlice = []string{
			c.GetMsg(code),
		}
	)
	if len(msg) != 0 {
		for _, v := range msg {
			switch a := v.(type) {
			case error:
				msgSlice = append(msgSlice, a.Error())
			case string:
				msgSlice = append(msgSlice, a)
			}
		}
	}
	msgStr := strings.Join(msgSlice, ", ")
	return gerror.NewCode(gcode.New(code, "", nil), msgStr)
}
统一响应数据中间件
设计统一响应数据的中间件,并且注入到 HTTP 请求流程中:
/internal/logic/middleware/response.go
type sMiddleware struct {
}
func init() {
	service.RegisterMiddleware(New())
}
func New() *sMiddleware {
	return &sMiddleware{}
}
type Response struct {
	Code    int         `json:"code"    dc:"业务码"`
	Message string      `json:"message" dc:"业务码说明"`
	Data    interface{} `json:"data"    dc:"返回的数据"`
}
func (s *sMiddleware) Response(r *ghttp.Request) {
	r.Middleware.Next()
	if r.Response.BufferLength() > 0 {
		return
	}
	// 先过滤掉服务器内部错误
	if r.Response.Status >= http.StatusInternalServerError {
		// 清除掉缓存区,防止服务器信息泄露到客户端
		r.Response.ClearBuffer()
		r.Response.Writeln("服务器打盹了,请稍后再来找他!")
	}
	var (
		res  = r.GetHandlerResponse()
		err  = r.GetError()
		code = gerror.Code(err)
		msg  string
	)
	if err != nil {
		msg = err.Error()
	} else {
		code = gcode.CodeOK
		msg = packed.Err.GetMsg(code.Code())
	}
	r.Response.WriteJson(Response{
		Code:    code.Code(),
		Message: msg,
		Data:    res,
	})
}
/internal/cmd/cmd.go
s.Group("/", func(group *ghttp.RouterGroup) {
	group.Middleware(service.Middleware().Response)
	group.Bind(
		exception.NewV1(),
	)
})
结果
然后在服务文件中调用 packed/err
/internal/logic/exception/exception.go:
func (s *sException) Business() error {
	return packed.Err.Skip(10001)
}
// System 这里我们对 gjson.Decode() 传入错误数据,用来模拟组件内部抛出err
func (s *sException) System() error {
	_, err := gjson.Decode("")
	if err != nil {
		return packed.Err.Sys("可选自定义信息")
	}
	return nil
}
结果展示:
Business
{
	"code": 10001,
	"message": "用户名或密码错误",
	"data": null
}
System
{
	"code": 99999,
	"message": "未知错误, 可选自定义信息",
	"data": null
}
用户名或密码错误的业务异常也不会再抛出堆栈异常了:

尾声
上述的代码例子已经开源在:Github
本博客源码使用的也是这种 err 设计,想要了解更多可以查看:Github/oldme-api
GoFrame 优化接口的错误码和异常的思路的更多相关文章
- ErrorCode枚举类型返回错误码信息测试,手动抛出异常信息,在事务中根据错误码来回滚事务的思路。
		
ErrorCode.java 简单测试代码,具体应用思路:手动抛出异常信息,在事务中根据错误码来回滚事务的思路. public enum ErrorCode { //系统级 SUCCESS(" ...
 - Java异常封装(自己定义错误码和描述,附源码)
		
真正工作了才发现,Java里面的异常在真正工作中使用还是十分普遍的.什么时候该抛出什么异常,这个是必须知道的. 当然真正工作里面主动抛出的异常都是经过分装过的,自己可以定义错误码和异常描述. 下面小宝 ...
 - Java异常封装(自定义错误码和描写叙述,附源代码)
		
真正工作了才发现.Java里面的异常在真正工作中使用还是十分普遍的. 什么时候该抛出什么异常,这个是必须知道的. 当然真正工作里面主动抛出的异常都是经过分装过的,自己能够定义错误码和异常描写叙述. 以 ...
 - 写给初学者的Linux errno 错误码机制
		
不同于Java的异常处理机制, 当你使用C更多的接触到是基于错误码的异常机制, 简单来说就是当调用的函数发生异常时, 程序不会跳转到一个统一处理异常的地方, 取而代之的是返回一个整型错误码. 可能会有 ...
 - 使用whistle模拟cgi接口异常:错误码、502、慢网速、超时
		
绝大多数程序只考虑了接口正常工作的场景,而用户在使用我们的产品时遇到的各类异常,全都丢在看似 ok 的 try catch 中.如果没有做好异常的兼容和兜底处理,会极大的影响用户体验,严重的还会带来安 ...
 - Spring/SpringBoot定义统一异常错误码返回
		
配置 大致说下流程, 首先我们自定义一个自己的异常类CustomException,继承RuntimeException.再写一个异常管理类ExceptionManager,用来抛出自定义的异常. 然 ...
 - [2017-08-28]Abp系列——业务异常与错误码设计及提示语的本地化
		
本系列目录:Abp介绍和经验分享-目录 前言 ABP中有个异常UserFriendlyException经常被使用,但是它所在的命名空间是Abp.UI,总觉得和展现层联系过于紧密,在AppServic ...
 - JavaWeb项目中获取对Oracle操作时抛出的异常错误码
		
最近在项目中碰到了这么一个需求,一个JavaWeb项目,数据库用的是Oracle.业务上有一个对一张表的操作功能,当时设置了两个字段联合的唯一约束.由于前断没有对重复字段的校验,需要在插入时如果碰到唯 ...
 - .NET中异常与错误码优劣势对比
		
.NET之所以选择异常,而不是返回错误码来报告异常,是由于前者有以下几个优势: 1.异常与oop语言的结合性更好.oop语言经常需要对成员签名强加限制,比如c#中的构造函数.操作符重载和属性,开发者对 ...
 - Redis Windows 服务启动异常 错误码1067
		
https://blog.csdn.net/after_you/article/details/62215163 Redis Windows 服务启动异常 错误码1067 下载了Redis 2.8.2 ...
 
随机推荐
- 4.5 C++ Boost 文件目录操作库
			
Boost 库是一个由C/C++语言的开发者创建并更新维护的开源类库,其提供了许多功能强大的程序库和工具,用于开发高质量.可移植.高效的C应用程序.Boost库可以作为标准C库的后备,通常被称为准标准 ...
 - Mybatis(一对一、一对多、多对多)操作
			
* 首先列出示例中用到的数据库表 user表: accout表: role表: user_role表: 建表语句如下: DROP TABLE IF EXISTS `user`; CREATE TABL ...
 - locate命令找不到,但是实际文件存在的情况
			
locate和find命令都是linux下常用的搜索命令,但是locate命令是从一个数据库里面搜索的,它的速度比find查找要快上不少.如果存在某个文件用locate查不到的话,那么可以用upd ...
 - 2023年多校联训NOIP层测试6
			
2023年多校联训NOIP层测试6 打了 \(10min\) 骗分,就溜了. T1 弹珠游戏 \(0pts\) 没听懂讲评,暂时咕了. T2 晚会 \(20pts\) 部分分( \(20pts\) ) ...
 - NEMU PA 3-3 实验报告
			
一.实验目的 在上一章PA3-2中,我们实现了分段机制,将48位的虚拟地址vaddr转换成了laddr.为什么不是paddr呢?这就要说到这一章要完成的东西:**分页机制 **. 从80386开始,计 ...
 - Spring Boot图书管理系统项目实战-3.用户登录
			
导航: pre: 2.项目搭建 next:4.基础信息管理 只挑重点的讲,具体的请看项目源码. 1.项目源码 需要源码的朋友,请捐赠任意金额后留下邮箱发送:) 2.登录页设计 <!DOCTYP ...
 - ORACLE SEQUENCE 详解
			
1. About Sequences(关于序列) 序列是数据库对象一种.多个用户可以通过序列生成连续的数字以此来实现主键字段的自动.唯一增长,并且一个序列可为多列.多表同时使用. 序列消除了串行 ...
 - 24个javascript最佳实践
			
1. 使用 === 代替 == JavaScript utilizes two different kinds of equality operators: === | !== and == | != ...
 - qt基础知识总结
			
qt基础知识总结 1.ctrl+r:快速运行 2.两种模式的区别: 一个是提供菜单栏的,一个不提供菜单栏 3.界面讲解 layouts:布局=水平布局+垂直布局+网格布局+表单布局 spacers:垫 ...
 - letcode-括号生成
			
递归大法,空间换时间 就是记录左右括号数,一旦右括号数大于左括号数,退出. 当左右括号数相等,且等于n则为合法解. 使用char数组取代StringBuilder可以减少内存使用,这样每次进行回溯时不 ...