前言

你是否想在使用 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
}

这个例子模拟了一个完整的接口,从 apicontroller 到 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
}

优化方案

此时,我们的接口中有三个不足:

  1. 业务异常不应该抛出堆栈,因为用户名或密码错误的堆栈没有意义;
  2. 系统异常的响应信息中, message 不应该抛出 "json Decode failed: EOF",应该使用 未知错误 或者 系统错误 这类字眼;
  3. 业务异常和系统异常的业务码,也就是响应信息中的 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 优化接口的错误码和异常的思路的更多相关文章

  1. ErrorCode枚举类型返回错误码信息测试,手动抛出异常信息,在事务中根据错误码来回滚事务的思路。

    ErrorCode.java 简单测试代码,具体应用思路:手动抛出异常信息,在事务中根据错误码来回滚事务的思路. public enum ErrorCode { //系统级 SUCCESS(" ...

  2. Java异常封装(自己定义错误码和描述,附源码)

    真正工作了才发现,Java里面的异常在真正工作中使用还是十分普遍的.什么时候该抛出什么异常,这个是必须知道的. 当然真正工作里面主动抛出的异常都是经过分装过的,自己可以定义错误码和异常描述. 下面小宝 ...

  3. Java异常封装(自定义错误码和描写叙述,附源代码)

    真正工作了才发现.Java里面的异常在真正工作中使用还是十分普遍的. 什么时候该抛出什么异常,这个是必须知道的. 当然真正工作里面主动抛出的异常都是经过分装过的,自己能够定义错误码和异常描写叙述. 以 ...

  4. 写给初学者的Linux errno 错误码机制

    不同于Java的异常处理机制, 当你使用C更多的接触到是基于错误码的异常机制, 简单来说就是当调用的函数发生异常时, 程序不会跳转到一个统一处理异常的地方, 取而代之的是返回一个整型错误码. 可能会有 ...

  5. 使用whistle模拟cgi接口异常:错误码、502、慢网速、超时

    绝大多数程序只考虑了接口正常工作的场景,而用户在使用我们的产品时遇到的各类异常,全都丢在看似 ok 的 try catch 中.如果没有做好异常的兼容和兜底处理,会极大的影响用户体验,严重的还会带来安 ...

  6. Spring/SpringBoot定义统一异常错误码返回

    配置 大致说下流程, 首先我们自定义一个自己的异常类CustomException,继承RuntimeException.再写一个异常管理类ExceptionManager,用来抛出自定义的异常. 然 ...

  7. [2017-08-28]Abp系列——业务异常与错误码设计及提示语的本地化

    本系列目录:Abp介绍和经验分享-目录 前言 ABP中有个异常UserFriendlyException经常被使用,但是它所在的命名空间是Abp.UI,总觉得和展现层联系过于紧密,在AppServic ...

  8. JavaWeb项目中获取对Oracle操作时抛出的异常错误码

    最近在项目中碰到了这么一个需求,一个JavaWeb项目,数据库用的是Oracle.业务上有一个对一张表的操作功能,当时设置了两个字段联合的唯一约束.由于前断没有对重复字段的校验,需要在插入时如果碰到唯 ...

  9. .NET中异常与错误码优劣势对比

    .NET之所以选择异常,而不是返回错误码来报告异常,是由于前者有以下几个优势: 1.异常与oop语言的结合性更好.oop语言经常需要对成员签名强加限制,比如c#中的构造函数.操作符重载和属性,开发者对 ...

  10. Redis Windows 服务启动异常 错误码1067

    https://blog.csdn.net/after_you/article/details/62215163 Redis Windows 服务启动异常 错误码1067 下载了Redis 2.8.2 ...

随机推荐

  1. 6.1 C/C++ 封装字符串操作

    C/C++语言是一种通用的编程语言,具有高效.灵活和可移植等特点.C语言主要用于系统编程,如操作系统.编译器.数据库等:C语言是C语言的扩展,增加了面向对象编程的特性,适用于大型软件系统.图形用户界面 ...

  2. 17.5 稀疏调拨的内存映射文件--《Windows核心编程》

    原文链接:https://www.likecs.com/show-306421749.html,原文中代码是C++MFC程序,更详细.本文是C语言测试代码. (1)稀疏文件(Sparse File)定 ...

  3. 【译】发布 .NET Aspire 预览版 2(一)

    原文 | Damian Edwards 翻译 | 郑子铭 自上个月宣布并推出 .NET Aspire 以来,我们收到的反馈非常惊人!通过问题和拉取请求对回购协议的参与一直激励着团队.我们正在深入了解开 ...

  4. 普及模拟2 +【LGR-155-Div.3】洛谷基础赛 #3 &「NnOI」Round 2

    普及模拟2 \(T1\) 地址 \(0pts\) 简化题意:判断一个 \(IP\) 地址是否合法(数据保证字符串中存在且仅存在4个被字符分开的整数),若不合法则将其改正. 部分分: \(0pts\) ...

  5. LGV引理

    LGV引理是用来统计DAG中固定若干起点和终点情况下的选择不相交链的方案数的. 同样用来优化计数问题,但是比Pólya定理友好多了,这也就是为什么它能够被直接糊到NOI考场上. 对于一张DAG,每条边 ...

  6. NC20164 [JSOI2008]最大数MAXNUMBER

    题目链接 题目 题目描述 现在请求你维护一个数列,要求提供以下两种操作: 1. 查询操作.语法:Q L 功能:查询当前数列中末尾L 个数中的最大的数,并输出这个数的值.限制:L不超过当前数列的长度. ...

  7. Shiro 框架的MD5加密算法实现原理

    直接上代码:该代码可以直接用于项目中做MD5加密,加盐加密,多层散列加密 import java.io.UnsupportedEncodingException; import java.securi ...

  8. keras建模的3种方式——序列模型、函数模型、子类模型

    1 前言 keras是Google公司于2016年发布的以tensorflow为后端的用于深度学习网络训练的高阶API,因接口设计非常人性化,深受程序员的喜爱. keras建模有3种实现方式--序列模 ...

  9. vue+element-ui项目搭建实战

    1.使用vue ui创建vue工程 利用vue-cli提供的图形化工具快速搭建vue工程: 命令行运行:vue ui 工程结构说明 build:项目构建webpack(打包器)相关代码 config: ...

  10. python课本学习-第一章

    chapter 1 python开发入门 1.python之父:Guido van Rossum 2.python语言的特征: 简单 易学 免费&开源 可移植性 解释性 面向对象 在面向对象的 ...