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办法,假如任意一个流程呈现过错,那么都可能导致程序错乱。

用魔法打败魔法,用Go代码写Go代码

解决思路

经过配置文件+代码生成

上面的代码,其实最主要的内容便是code和Message,剩余的内容大致相同,那么咱们能够将过错码提早编写一个对应的配置文件,包括过错码的code和message,编写一个Go程序,去解析配置,然后生成Go代码?

  1. 配置信息如下:
errx:
    10001:  
        InvalidParamError: '参数反常'  
    100002:  
        RecordNotFound: '未找到查询结果'  
    10003:  
        TopKInvalid: '超出topk约束'  
    50001:  
        InternalError: '系统反常'
  1. 运用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
}
  1. 测试,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即可自动为咱们生成新增的过错码对应的代码。