golang封装业务err
我们有时在web开发时,仅凭httpStatus以及msg是不方便维护和体现我们的业务逻辑的。所以就需要封装我们自己的业务错误。
- 自定义biz_err
- 维护err map:errorResponseMap、errorHttpStatusMap
注意:
本文主要以演示为主,主要是让大家熟悉封装自定义错误的思路,故而封装的较为简单。大家可根据自己公司需求来进行拓展。
代码仓库地址:https://github.com/ziyifast/ziyifast-code_instruction
项目结构:
1 err:自定义err,重写打印格式等
1.1 biz_err_demo/error/zerr/errors.go:new方法
- 重写控制台打印格式
- 封装new方法
- DefaultBizWrap:不含原始err
- BizWrap:包含原始err
package zerrimport ("errors""fmt""io"
)func New(message string) error {return &fundamental{msg: message,stack: callers(),}
}func Errorf(format string, args ...interface{}) error {return &fundamental{msg: fmt.Sprintf(format, args...),stack: callers(),}
}type fundamental struct {msg string*stack
}func (f *fundamental) Error() string { return f.msg }func (f *fundamental) Format(s fmt.State, verb rune) {switch verb {case 'v':if s.Flag('+') {io.WriteString(s, f.msg)f.stack.Format(s, verb)return}fallthroughcase 's':io.WriteString(s, f.msg)case 'q':fmt.Fprintf(s, "%q", f.msg)}
}func WithStack(err error) error {if err == nil {return nil}return &withStack{err,callers(),}
}type withStack struct {error*stack
}func (w *withStack) Cause() error { return w.error }func (w *withStack) Format(s fmt.State, verb rune) {switch verb {case 'v':if s.Flag('+') {fmt.Fprintf(s, "%+v", w.Cause())w.stack.Format(s, verb)return}fallthroughcase 's':io.WriteString(s, w.Error())case 'q':fmt.Fprintf(s, "%q", w.Error())}
}func Wrap(err error, message string) error {if err == nil {return nil}err = &withMessage{cause: err,msg: message,}return &withStack{err,callers(),}
}func Trace(err error) error {return Wrapf(err, "")
}func Wrapf(err error, format string, args ...interface{}) error {if err == nil {return nil}err = &withMessage{cause: err,msg: fmt.Sprintf(format, args...),}return &withStack{err,callers(),}
}func WithMessage(err error, message string) error {if err == nil {return nil}return &withMessage{cause: err,msg: message,}
}func WithMessagef(err error, format string, args ...interface{}) error {if err == nil {return nil}return &withMessage{cause: err,msg: fmt.Sprintf(format, args...),}
}type withMessage struct {cause errormsg string
}func (w *withMessage) Error() string {return w.msg + ": " + w.cause.Error()
}func (w *withMessage) Cause() error {return w.cause
}func (w *withMessage) Format(s fmt.State, verb rune) {switch verb {case 'v':if s.Flag('+') {fmt.Fprintf(s, "%+v\n", w.Cause())io.WriteString(s, w.msg)return}fallthroughcase 's', 'q':io.WriteString(s, w.Error())}
}func Cause(err error) error {type causer interface {Cause() error}for err != nil {cause, ok := err.(causer)if !ok {break}err = cause.Cause()}return err
}func WithCode(err error, code string) error {if err == nil {return nil}return &ErrWrap{cause: err,code: code,}
}func WithCodef(err error, format string, args ...interface{}) error {if err == nil {return nil}return &ErrWrap{cause: err,code: fmt.Sprintf(format, args...),}
}type ErrWrap struct {cause errorcode stringvars []string
}func (w *ErrWrap) Vars() []string {return w.vars
}func (w *ErrWrap) Code() string {return w.code
}func (w *ErrWrap) Error() string {var msg stringif w.cause != nil {msg += w.cause.Error()}return msg
}func (w *ErrWrap) Cause() error {return w.cause
}// Format rewrite format
func (w *ErrWrap) Format(s fmt.State, verb rune) {switch verb {case 'v':if s.Flag('+') {fmt.Fprintf(s, "%+v\n", w.Cause())io.WriteString(s, "BizCode=["+string(w.code)+"]")return}fallthroughcase 's', 'q':io.WriteString(s, w.Error())}
}func BizWrap(err error, code string, message string, vars ...string) error {if err == nil {return nil}codeErr := &ErrWrap{cause: err,code: code,vars: vars,}err = &withMessage{cause: codeErr,msg: message,}return &withStack{err,callers(),}
}func DefaultBizWrap(code string, vars ...string) error {err := errors.New("")codeErr := &ErrWrap{cause: err,code: code,vars: vars,}err = &withMessage{cause: codeErr,}return &withStack{err,callers(),}
}
1.2 biz_err_demo/error/zerr/stack.go:堆栈打印格式
定义堆栈打印格式
package zerrimport ("fmt""io""path""runtime""strings"
)type Frame uintptrfunc (f Frame) pc() uintptr { return uintptr(f) - 1 }func (f Frame) file() string {fn := runtime.FuncForPC(f.pc())if fn == nil {return "unknown"}file, _ := fn.FileLine(f.pc())return file
}func (f Frame) line() int {fn := runtime.FuncForPC(f.pc())if fn == nil {return 0}_, line := fn.FileLine(f.pc())return line
}func (f Frame) Format(s fmt.State, verb rune) {switch verb {case 's':switch {case s.Flag('+'):pc := f.pc()fn := runtime.FuncForPC(pc)if fn == nil {io.WriteString(s, "unknown")} else {file, _ := fn.FileLine(pc)fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)}default:io.WriteString(s, path.Base(f.file()))}case 'd':fmt.Fprintf(s, "%d", f.line())case 'n':name := runtime.FuncForPC(f.pc()).Name()io.WriteString(s, funcname(name))case 'v':f.Format(s, 's')io.WriteString(s, ":")f.Format(s, 'd')}
}type StackTrace []Framefunc (st StackTrace) Format(s fmt.State, verb rune) {switch verb {case 'v':switch {case s.Flag('+'):for _, f := range st {fmt.Fprintf(s, "\n%+v", f)}case s.Flag('#'):fmt.Fprintf(s, "%#v", []Frame(st))default:fmt.Fprintf(s, "%v", []Frame(st))}case 's':fmt.Fprintf(s, "%s", []Frame(st))}
}type stack []uintptrfunc (s *stack) Format(st fmt.State, verb rune) {switch verb {case 'v':switch {case st.Flag('+'):for _, pc := range *s {f := Frame(pc)fmt.Fprintf(st, "\n%+v", f)}}}
}func (s *stack) StackTrace() StackTrace {f := make([]Frame, len(*s))for i := 0; i < len(f); i++ {f[i] = Frame((*s)[i])}return f
}func callers() *stack {const depth = 32var pcs [depth]uintptrn := runtime.Callers(3, pcs[:])if n > 1 {n = 1}var st stack = pcs[0:n]return &st
}func funcname(name string) string {i := strings.LastIndex(name, "/")name = name[i+1:]i = strings.Index(name, ".")return name[i+1:]
}
1.3 biz_err_demo/error/zerr/wrap.go:判断err类型
由自定义err,判断是否属于某个err
package zerrimport ("errors""reflect"
)func Unwrap(err error) error {u, ok := err.(interface {Cause() error})if !ok {return errors.Unwrap(err)}return u.Cause()
}func Is(err, target error) bool {if target == nil {return err == target}isComparable := reflect.TypeOf(target).Comparable()for {if isComparable && err == target {return true}if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {return true}if err = Unwrap(err); err == nil {return false}}
}func As(err error, target interface{}) bool {if target == nil {panic("errors: target cannot be nil")}val := reflect.ValueOf(target)typ := val.Type()if typ.Kind() != reflect.Ptr || val.IsNil() {panic("errors: target must be a non-nil pointer")}targetType := typ.Elem()if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) {panic("errors: *target must be interface or implement error")}for err != nil {if reflect.TypeOf(err).AssignableTo(targetType) {val.Elem().Set(reflect.ValueOf(err))return true}if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {return true}err = Unwrap(err)}return false
}var errorType = reflect.TypeOf((*error)(nil)).Elem()
1.4 biz_err_demo/error/biz_err/code.go:业务错误码
自定义业务错误码、对应错误信息及对应错误对应的httpStatusCode
package biz_errimport ("myTest/demo_home/biz_err_demo/error/zerr""net/http""strings"
)const (Undefined = "Undefined"OsCreateFileError = "OsCreateFileError"ImageNotSupported = "ImageNotSupported"UsernameOrPasswordInValid = "UsernameOrPasswordInValid"
)var errorResponseMap = map[string]string{OsCreateFileError: "创建文件失败",ImageNotSupported: "图片格式不支持",UsernameOrPasswordInValid: "用户名或密码错误",
}var errorHttpStatusMap = map[string]int{OsCreateFileError: http.StatusInternalServerError,ImageNotSupported: http.StatusInternalServerError,UsernameOrPasswordInValid: http.StatusInternalServerError,
}func ParseBizErr(err error) (httpStatus int, code, msg string) {if err == nil {code = Undefined}vars := make([]string, 0)errWrap := new(zerr.ErrWrap)var cause errorif as := zerr.As(err, &errWrap); as {code = errWrap.Code()cause = errWrap.Cause()vars = errWrap.Vars()} else {code = Undefined}if code == Undefined {var undefinedMsg stringif err != nil {undefinedMsg = err.Error()}if undefinedMsg == "" || undefinedMsg == ": " {undefinedMsg = errorResponseMap[code]}return errorHttpStatusMap[code], code, undefinedMsg}if status, ok := errorHttpStatusMap[code]; ok {httpStatus = status} else {httpStatus = http.StatusOK}if bizMsg, ok := errorResponseMap[code]; ok {for _, v := range vars {bizMsg = strings.Replace(bizMsg, "%s", v, 1)}msg = bizMsgif cause != nil {_, _, causeMsg := ParseBizErr(cause)if causeMsg != "" {msg += ", " + causeMsg} else {msg += ", " + errWrap.Error()}}} else {msg = errWrap.Error()}return httpStatus, code, msg
}func ErrResponse(err error) (httpStatus int, code, msg string) {if err == nil {code = Undefined}vars := make([]string, 0)errWrap := new(zerr.ErrWrap)var cause errorif as := zerr.As(err, &errWrap); as {code = errWrap.Code()cause = errWrap.Cause()vars = errWrap.Vars()} else {code = Undefined}if status, ok := errorHttpStatusMap[code]; ok {httpStatus = status} else {httpStatus = http.StatusOK}if bizMsg, ok := errorResponseMap[code]; ok {for _, v := range vars {bizMsg = strings.Replace(bizMsg, "%s", v, 1)}msg = bizMsgif cause != nil {_, _, causeMsg := ErrResponse(cause)if causeMsg != "" {msg += causeMsg} else {msg += errWrap.Error()}}} else {msg = errWrap.Error()}return httpStatus, code, msg
}
2 controller:封装base_controller
2.1 biz_err_demo/constant/constant.go
package constantconst (ContentTypeJson = "application/json"ContentTypeXml = "application/xml"
)
2.2 biz_err_demo/controller/base_controller.go
package controllerimport ("encoding/json""encoding/xml""github.com/kataras/iris/v12""github.com/kataras/iris/v12/mvc""github.com/sirupsen/logrus""myTest/demo_home/biz_err_demo/constant""myTest/demo_home/biz_err_demo/error/biz_err""myTest/demo_home/biz_err_demo/response""net/http"
)type BaseController struct {Ctx iris.Context
}func commonResp(errMsg string, httpCode int, returnCode response.Code, content interface{}) mvc.Response {payload := &response.JsonResponse{Code: returnCode,Msg: errMsg,Content: content,}contentDetail, err := json.Marshal(payload)if err != nil {logrus.Infof("marshal json response error %v", err)}return mvc.Response{Code: httpCode,Content: contentDetail,ContentType: constant.ContentTypeJson,}
}func (c *BaseController) Xml(httpCode int, content interface{}) mvc.Response {payload, err := xml.Marshal(content)if err != nil {logrus.Errorf("marshal xml response error %v", err)}return c.XmlRaw(httpCode, payload)
}func (c *BaseController) XmlOK(content interface{}) mvc.Response {payload, err := xml.Marshal(content)if err != nil {logrus.Errorf("marshal xml response error %v", err)}return c.XmlRaw(http.StatusOK, payload)
}func (c *BaseController) XmlRaw(httpCode int, content []byte) mvc.Response {return mvc.Response{Code: httpCode,Content: content,ContentType: constant.ContentTypeXml,}
}func (c *BaseController) JsonBizError(err error) mvc.Response {httpStatus, code, msg := biz_err.ErrResponse(err)return commonResp(msg, httpStatus, response.Code(code), nil)
}
2.3 biz_err_demo/controller/test_biz_controller.go
package controllerimport ("errors""github.com/kataras/iris/v12/mvc""myTest/demo_home/biz_err_demo/error/biz_err""myTest/demo_home/biz_err_demo/error/zerr""myTest/demo_home/biz_err_demo/response""net/http"
)type TestBizController struct {BaseController
}func (t *TestBizController) BeforeActivation(b mvc.BeforeActivation) {b.Handle(http.MethodGet, "/testBizErr", "TestBizErr")
}func (t *TestBizController) TestBizErr() mvc.Result {err1 := errors.New("")err := zerr.BizWrap(err1, biz_err.UsernameOrPasswordInValid, "")return response.JsonBizError(err)
}
3 封装response
3.1 biz_err_demo/response/json_response.go
package responseimport ("encoding/json""github.com/kataras/iris/v12/mvc""github.com/sirupsen/logrus""myTest/demo_home/biz_err_demo/constant""myTest/demo_home/biz_err_demo/error/biz_err"
)type Code stringtype JsonResponse struct {Code Code `json:"code"`Msg string `json:"msg"`Content interface{} `json:"content,omitempty"`
}func JsonBizError(err error) mvc.Response {httpStatus, code, msg := biz_err.ErrResponse(err)return commonResp(msg, httpStatus, Code(code), nil)
}func commonResp(errMsg string, httpCode int, returnCode Code, content interface{}) mvc.Response {payload := &JsonResponse{Code: returnCode,Msg: errMsg,Content: content,}contentDetail, err := json.Marshal(payload)if err != nil {logrus.Errorf("%v", err)}return mvc.Response{Code: httpCode,Content: contentDetail,ContentType: constant.ContentTypeJson,}
}
4 测试效果
4.1 biz_err_demo/test/main.go
package mainimport ("errors""github.com/sirupsen/logrus""myTest/demo_home/biz_err_demo/error/biz_err""myTest/demo_home/biz_err_demo/error/zerr"
)func init() {logrus.SetReportCaller(true) // 设置日志是否记录被调用的位置,默认值为 false
}func main() {TestWithNoSourceErr()TestWithSourceErr()TestParseBizErr()
}func TestWithNoSourceErr() {err := zerr.DefaultBizWrap(biz_err.UsernameOrPasswordInValid, "")logrus.Errorf("TestWithNoSourceErr %+v", err)
}func TestWithSourceErr() {err := errors.New("invalid image")err = zerr.BizWrap(err, biz_err.ImageNotSupported, "")logrus.Errorf("TestWithSourceErr %+v", err)
}func TestParseBizErr() {err := errors.New("")err = zerr.BizWrap(err, biz_err.ImageNotSupported, "")httpStatus, bizCode, msg := biz_err.ParseBizErr(err)logrus.Errorf("httpStatus:%d bizCode:%s msg:%s", httpStatus, bizCode, msg)
}
4.2 biz_err_demo/main.go
package mainimport ("github.com/kataras/iris/v12""github.com/kataras/iris/v12/mvc""myTest/demo_home/biz_err_demo/controller"
)func main() {app := iris.New()mvc.New(app).Handle(new(controller.TestBizController))app.Listen(":8088", nil)
}
这样前端就能直接根据我们的业务错误码展示对应msg信息