error


背景

HAS的error设计,是从作者个人经验和痛苦经历的出发设计的。不同于大多数使用wrap函数构建error抛出栈的方式——这大概是来源于Java的设计思想——,作者认为最重要的了解错误出现的函数调用栈,而没有必要把上层一些错误描述包裹进去。 另一方面,error.Error()字符串的比较总觉得不是一个得体的方法。

此外,golang的error没有错误类型的概念。作者曾经经历的项目遇到过如下特别痛苦的事情:当后端接口向前端返回error时,error的粒度很难控制。例如,错误原因可能是因为前端少传一个参数,对于前端来说最友好的方式,是告知前端,缺少了某个参数。但是如果这个错误信息暴露给用户(假设这是一个没有被发现的bug,不小心被用户触发了),这样的错误信息对于用户来说又是非常的友好的。如果让前端根据错误描述的不同,向用户展示不同的信息(例如“系统错误”),对于前端同学而言又是个繁重的体力劳动,更别说后端定义error string的随意性让前端同学更加没法做到面面俱到。当然,理想的方式是后端把所有error都预先定义出来,但是这样的做法在敏捷开发大行其道的今天,又有多少开发团队能够做到呢。

HAS的error设计希望能够实际的解决上面的问题。

目的

HAS的error处理希望能够达到两个目的:

  1. error具有类型和描述,便于前端根据类型对错误进行灵活处理。类型一般可以预定义,而描述则可以根据程序/应用的实际情况相对随意的设置
  2. 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,可参考自定义Serviceslot.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/56d1aa7de1e8f373a137e2623180271eopen in new window

小故事

fingerprint的出现来源于我们曾遇到的一个真实情况

我们的系统已经交付部署给客户,但客户提出有问题,而开发团队和测试团队都无法复现问题。

客户允许远程连接一台电脑在局域网服务系统,但不能连接服务器,也就无法获取程序的log

甚至有些客户连远程连接都不允许,只能现场去看,现场屏幕都不能拍照,而开发团队和客户不在同一城市,相距1800+km,让开发团队派人出差去看个log又回来不太现实,成本也高, 此时我们只能让当地的交付部门同事去客户现场帮忙查看情况,但面对每秒刷新几百行的log,根本无法有效的定位问题

以上2种情况其实并不常见,但确实是我们遇到过的,基于特殊情况,我们需要一个快速定位错误的解决方案,fingerprint应运而生。