error
背景
HAS的error设计,是从作者个人经验和痛苦经历的出发设计的。不同于大多数使用wrap函数构建error抛出栈的方式——这大概是来源于Java的设计思想——,作者认为最重要的了解错误出现的函数调用栈,而没有必要把上层一些错误描述包裹进去。 另一方面,error.Error()字符串的比较总觉得不是一个得体的方法。
此外,golang的error没有错误类型的概念。作者曾经经历的项目遇到过如下特别痛苦的事情:当后端接口向前端返回error时,error的粒度很难控制。例如,错误原因可能是因为前端少传一个参数,对于前端来说最友好的方式,是告知前端,缺少了某个参数。但是如果这个错误信息暴露给用户(假设这是一个没有被发现的bug,不小心被用户触发了),这样的错误信息对于用户来说又是非常的友好的。如果让前端根据错误描述的不同,向用户展示不同的信息(例如“系统错误”),对于前端同学而言又是个繁重的体力劳动,更别说后端定义error string的随意性让前端同学更加没法做到面面俱到。当然,理想的方式是后端把所有error都预先定义出来,但是这样的做法在敏捷开发大行其道的今天,又有多少开发团队能够做到呢。
HAS的error设计希望能够实际的解决上面的问题。
目的
HAS的error处理希望能够达到两个目的:
- error具有类型和描述,便于前端根据类型对错误进行灵活处理。类型一般可以预定义,而描述则可以根据程序/应用的实际情况相对随意的设置
- error可以具有函数调用栈,便于工程师对一些错误(尤其是哪些被大量调用的底层公共函数)进行定位。
定义error
HAS的error定义在has/common/herror中。结构如下:
type Error struct {
Code int `json:"code"`
Desc string `json:"desc"`
Fingerprint string `json:"fingerprint,omitempty"`
Cause string `json:"cause"`
stack []string
}
如上所示,error包括了三个字段:
| 字段 | 解释 |
|---|---|
| code | 错误编码,可以用来代表错误的类型 |
| desc | 错误描述,可以根据应用需要设置 |
| fingerprint | 错误id,用于在无法获取log的情况下获取堆栈 |
| stack | 错误出现的函数调用栈 |
herrors.Error的方法有:
| 方法 | 解释 |
|---|---|
| Error() string | 实现golang error接口,输出desc |
| Code() int | 输出error code |
| Equal(err *Error) bool | 比较两个herrors.error是否相等,通过code比较 |
| Format() string | 输出错误信息,包括code、desc和callStack(如果不为空的话) |
| AddCallStack() | 为错误添加函数调用栈 |
| SetDesc(desc string) | 设置错误描述 |
创建error
HAS提供了3个创建herrors.error的方法:
| 方法 | 解释 |
|---|---|
| New(code int, desc string) | 新建error,并设置code和desc |
| NewWithStack(code int, desc string) | 新建error,并设置code和desc,同时获取当前函数的调用栈 |
| NewWithCode(code int) | 新建error,只设置code,后续根据业务需要设置desc |
code定义
为了避免error code冲突,HAS将 0-999预留为HAS框架使用。目前HAS预定义的code范围为:
| code | 说明 |
|---|---|
| 0 | 没有错误 |
| 1** | 服务错误 |
| 2** | 调用方错误 |
| 3** | 用户相关错误,例如密码错误 |
HAS预定义的code包括:
const (
ECodeOK = 0
// ECodeSystemInternal 服务器错误代码
ECodeSysInternal = 101 //服务器内部错误
ECodeSysBusy = 102 //服务器忙
//调用方错误代码
ECodeCallerInvalidApiRequest = 201 //无效API请求
ECodeCallerUnauthorizedApiAccess = 202 //非法API请求
ECodeCallerInvalidServiceRequest = 221 //无效Service请求
ECodeCallerUnauthorizedServiceRequest = 222 //非法Service请求
//用户端错误
ECodeUserInvalidAct = 301 // 无效用户行为
ECodeUserUnauthorizedAct = 302 // 非法用户行为
)
预定义error
HAS预定义了一批error,使用HAS进行后端服务编程时可以直接使用:
var (
// Backend errors
ErrSysInternal = New(ECodeSysInternal, "system internal error")
ErrSysBusy = New(ECodeSysBusy, "system busy")
// Caller errors
ErrCallerInvalidApiRequest = New(ECodeCallerInvalidApiRequest, "invalid api request")
ErrCallerUnauthorizedApiAccess = New(ECodeCallerUnauthorizedApiAccess, "unauthorized api request")
ErrCallerInvalidServiceRequest = New(ECodeCallerInvalidServiceRequest, "invalid service request")
ErrCallerUnauthorizedServiceRequest = New(ECodeCallerUnauthorizedServiceRequest, "unauthorized service required")
// User errors
ErrUserInvalidAct = New(ECodeUserInvalidAct, "invalid user act")
ErrUserUnauthorizedAct = New(ECodeUserUnauthorizedAct, "unauthorized user act")
)
如前所属,上述预定义error的描述,可以根据实际情况进行修改
fingerprint
如何使用
在Debug模式下,返回一个error,可参考自定义Service中slot.go的26行
package testsvs
import (
"github.com/drharryhe/has/common/herrors"
"github.com/drharryhe/has/common/hlogger"
"github.com/drharryhe/has/core"
)
type TestRequest struct {
core.SlotRequestBase // 注意继承 core.SlotRequestBase
// 注意类型要带指针,才能使用data验证
Name *string `json:"name" param:"require"` // 名称
Change *bool `json:"change" param:"require"` // 是否改变name param:"require"代表此字段必传
}
// 我们建议在项目开发中,开发者为此处的每个字段都写上注释,便于后期维护
func (this *Service) TestSlot(req *TestRequest, res *core.SlotResponse) {
hlogger.Info("request name:", *req.Name) // HAS中带有log组件可以直接使用
hlogger.Info("request change:", *req.Change)
resultName := *req.Name
if *req.Change {
resultName = this.TestName
}
if resultName == "" {
// 定义error debug模式下 New里面的内容会直接以log输出 所以不需要单独打log
res.Error = herrors.ErrSysInternal.New("名称为空").D("描述错误")
return
}
// 返回数据
this.Response(res, map[string]interface{}{
"change": *req.Change,
"resultName": resultName,
}, nil)
}
当触发error时,会返回
{
"data": {},
"error": {
"code": 101,
"desc": "此处信息会返回前端",
"fingerprint": "56d1aa7de1e8f373a137e2623180271e",
"cause": "名称为空"
}
}
此时我们记录fingerprint的值
访问http://127.0.0.1:8989/error/query/:fingerprint即可查看错误的堆栈信息
以上述返回的fingerprint为例
访问:http://127.0.0.1:8989/error/query/56d1aa7de1e8f373a137e2623180271e
小故事
fingerprint的出现来源于我们曾遇到的一个真实情况
我们的系统已经交付部署给客户,但客户提出有问题,而开发团队和测试团队都无法复现问题。
客户允许远程连接一台电脑在局域网服务系统,但不能连接服务器,也就无法获取程序的log
甚至有些客户连远程连接都不允许,只能现场去看,现场屏幕都不能拍照,而开发团队和客户不在同一城市,相距1800+km,让开发团队派人出差去看个log又回来不太现实,成本也高, 此时我们只能让当地的交付部门同事去客户现场帮忙查看情况,但面对每秒刷新几百行的log,根本无法有效的定位问题
以上2种情况其实并不常见,但确实是我们遇到过的,基于特殊情况,我们需要一个快速定位错误的解决方案,fingerprint应运而生。