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 NewXXX
new methods must be added. If any process fails, it may cause program confusion.
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?
- The configuration information is as follows:
errx:
10001:
InvalidParamError: '参数异常'
100002:
RecordNotFound: '未找到查询结果'
10003:
TopKInvalid: '超出topk限制'
50001:
InternalError: '系统异常'
github.com/dave/jennifer
generate 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
}
- 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 TestGenCode
to automatically generate the code corresponding to the newly added error code for us.