Beat magic with magic, write Go code with Go code

GoHTTP service returns business status code design

Generally, the error interface will be implemented by involving a BizErr structure, and BizEr will contain specific error code information.

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  
}

For better and more standardized maintenance of error codes, you can use constants to define error codes and error description information in advance.

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 "未知错误"  
    }  
}

The corresponding BizErr is changed to

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  
}

In order to conveniently initialize an error, we can also write the NewXXX method to generate an error, such as:

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  
    }  
}

This is the entire design process for custom errors that routinely handle HTTP business status codes.

problem thinking

Just imagine, if there is a new business that needs to return a new status code, then the corresponding constant list, e.Message()method must be added case, and NewXXXnew methods must be added. If any process fails, it may cause program confusion.

v2_ccc0a69c-db86-45c9-926f-eb1908099f4g.gif

Solutions

Through configuration files + code generation

The above code, in fact, the most important content is the code and Message, and the rest of the content is roughly the same, then we can write a corresponding configuration file in advance for the error code, including the code and message of the error code, write a Go program to parse configure, and then generate Go code?

  1. The configuration information is as follows:
errx:  
    10001:  
        InvalidParamError: '参数异常'  
    100002:  
        RecordNotFound: '未找到查询结果'  
    10003:  
        TopKInvalid: '超出topk限制'  
    50001:  
        InternalError: '系统异常'
  1. github.com/dave/jennifergenerate code using

This code library is specially used to generate code, and you can read his ReadME for details. The code to generate the code is as follows:

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. For testing, errx.yaml is an error code configuration file, which is placed in the statistics directory of the code.
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)
	}
}

In this way, when adding an error status code, you only need to write a configuration file, and then execute it TestGenCodeto automatically generate the code corresponding to the newly added error code for us.

Guess you like

Origin juejin.im/post/7250374485568487483