GoHTTP服务回来事务状况码设计
一般会经过涉及一个BizErr结构体,实现error接口,BizEr中会包括具体的过错码信息。
type BizError struct {
// 原始过错(可能为空)
Cause error
// 回来具体的事务状况码
Code int
// 回来具体的过错描绘
Message string
}
func (e *BizError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s:%s", e.Message, e.Cause)
}
return e.Message
}
为了更好的更规范的保护过错码,能够运用,能够经过常量的方式,去提早定义好过错码,以及过错描绘信息。
type ErrorCode int32
const (
InternalError = ErrorCode(int32(50001))
InvalidParamError = ErrorCode(int32(10001))
RecordNotFound = ErrorCode(int32(100002))
TopKInvalid = ErrorCode(int32(10003))
)
func (e ErrorCode) Message() string {
switch e {
case InternalError:
return "系统反常"
case InvalidParamError:
return "参数反常"
case RecordNotFound:
return "未找到查询结果"
case TopKInvalid:
return "超出topk约束"
default:
return "不知道过错"
}
}
对应的BizErr改为
type BizError struct {
Cause error
Code ErrorCode
Message string
}
func (e BizError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s:%s", e.Message, e.Cause)
}
return e.Message
}
为了便利初始化一个过错,咱们也能够编写NewXXX办法来生成过错,如:
func NewInvalidParamError(ops ...Option) error {
var bizErr = &BizError{
Code: InvalidParamError,
Message: InvalidParamError.Message(),
}
for _, op := range ops {
op(bizErr)
}
return errors.WithStack(bizErr)
}
func NewRecordNotFound(ops ...Option) error {
var bizErr = &BizError{
Code: RecordNotFound,
Message: RecordNotFound.Message(),
}
for _, op := range ops {
op(bizErr)
}
return errors.WithStack(bizErr)
}
type Option func(*BizError)
func Msg(ErrorMessage string, args ...interface{}) Option {
return func(bizErr *BizError) {
bizErr.Message = fmt.Sprintf(ErrorMessage, args...)
}
}
func Cause(err error) Option {
return func(bizError *BizError) {
bizError.Cause = err
}
}
这是日常处理HTTP事务状况码的自定义error的悉数设计流程。
问题思考
试想一下,假如有新的事务,需求回来新的状况码时,那么对应的常量列表,e.Message()
办法要加case
,还要新增NewXXX
办法,假如任意一个流程呈现过错,那么都可能导致程序错乱。
解决思路
经过配置文件+代码生成
上面的代码,其实最主要的内容便是code和Message,剩余的内容大致相同,那么咱们能够将过错码提早编写一个对应的配置文件,包括过错码的code和message,编写一个Go程序,去解析配置,然后生成Go代码?
- 配置信息如下:
errx:
10001:
InvalidParamError: '参数反常'
100002:
RecordNotFound: '未找到查询结果'
10003:
TopKInvalid: '超出topk约束'
50001:
InternalError: '系统反常'
- 运用
github.com/dave/jennifer
生成代码
该代码库,专门用来生成代码,具体内容可阅览他的ReadME。 生成代码的代码如下:
package errx
import (
_ "embed"
"errors"
"fmt"
"github.com/dave/jennifer/jen"
)
func GenFile(errMap map[int32]map[string]string, pkgName string, downloadPath string, withStack bool) error {
gf, err := genFile(errMap, pkgName, withStack)
if err != nil {
return err
}
return gf.Save(downloadPath)
}
func genFile(errMap map[int32]map[string]string, pkgName string, withStack bool) (*jen.File, error) {
file := jen.NewFile(pkgName)
file.HeaderComment("// Code generated by errx_generate. DO NOT EDIT.")
file.Type().Id("BizError").Struct(
jen.Id("Cause").Id("error"),
jen.Id("Code").Id("ErrorCode"),
jen.Id("Message").String(),
)
file.Func().Params(jen.Id("e").Id("BizError")).Id("Error").Params().String().Block(
jen.If(jen.Id("e").Dot("Cause").Op("!=").Nil()).Block(
jen.Return(jen.Qual("fmt", "Sprintf").Call(jen.Lit("%s:%s"), jen.Id("e").Dot("Message"), jen.Id("e").Dot("Cause"))),
),
jen.Return(jen.Id("e").Dot("Message")),
)
file.Type().Id("ErrorCode").Int32()
var errorConsts []jen.Code
var errorCases []jen.Code
for code, errDesp := range errMap {
if len(errDesp) != 1 {
return nil, errors.New("invalid config")
}
for errName, errMsg := range errDesp {
errConstItem := jen.Id(errName).Op("=").Id("ErrorCode").Parens(jen.Lit(code))
errorConsts = append(errorConsts, errConstItem)
errCaseItem := jen.Case(jen.Id(errName)).Block(jen.Return(jen.Lit(errMsg)))
errorCases = append(errorCases, errCaseItem)
}
}
errorCases = append(errorCases, jen.Default().Block(jen.Return(jen.Lit("不知道过错"))))
file.Const().Defs(errorConsts...)
file.Func().
Params(jen.Id("e").Id("ErrorCode")).
Id("Message").Params().
String().
Block(
jen.Switch(jen.Id("e")).Block(errorCases...),
)
retCode := jen.Return(jen.Id("bizErr"))
if withStack {
retCode = jen.Return(jen.Qual("github.com/pkg/errors", "WithStack").Call(jen.Id("bizErr")))
}
for _, errDesp := range errMap {
for errName := range errDesp {
file.Func().Id(fmt.Sprintf("New%s", errName)).Params(jen.Id("ops").Op(" ...").Id("Option")).Id("error").Block(
jen.Var().Id("bizErr").Op("=").Op("&").Id("BizError").Values(
jen.Dict{
jen.Id("Code"): jen.Id(errName),
jen.Id("Message"): jen.Id(errName).Dot("Message").Call(),
},
),
jen.For(jen.List(jen.Id("_"), jen.Id("op")).Op(":=").Range().Id("ops")).Block(
jen.Id("op").Call(jen.Id("bizErr")),
),
retCode,
)
}
file.Line()
}
file.Type().Id("Option").Func().Params(jen.Id("*BizError"))
file.Func().Id("Msg").
Params(jen.Id("ErrorMessage").String(), jen.Id("args").Op("...").Interface()).Id("Option").
Block(
jen.Return(jen.Func().Params(jen.Id("bizErr").Op("*").Id("BizError")).
Block(
jen.Id("bizErr").Dot("Message").Op("=").Qual("fmt", "Sprintf").Call(jen.Id("ErrorMessage"), jen.Id("args").Op("...")),
)),
)
file.Line()
file.Func().Id("Cause").Params(jen.Id("err").Error()).Id("Option").
Block(
jen.Return(jen.Func().Params(jen.Id("bizError").Op("*").Id("BizError")).Block(
jen.Id("bizError").Dot("Cause").Op("=").Id("err"),
)),
)
return file, nil
}
- 测试,errx.yaml是过错码配置文件,放到跟代码的计算目录。
package errx
import (
_ "embed"
"gopkg.in/yaml.v2"
"testing"
)
//go:embed errx.yaml
var errxConfRaw []byte
func TestGenCode(t *testing.T) {
type errConfStruct struct {
Errx map[int32]map[string]string `yaml:"errx"`
}
var errxConfStruct errConfStruct
if err := yaml.Unmarshal(errxConfRaw, &errxConfStruct); err != nil {
panic(err)
}
err := GenFile(errxConfStruct.Errx, "errx", "errx.go", true)
if err != nil {
t.Fatal(err)
}
}
这样,在新增过错状况码时,只需求编写配置文件,然后履行TestGenCode
即可自动为咱们生成新增的过错码对应的代码。