我们有时在web开发时,仅凭httpStatus以及msg是不方便维护和体现我们的业务逻辑的。所以就需要封装我们自己的业务错误。
- 自定义biz_err
- 维护err map:errorResponseMap、errorHttpStatusMap
注意:
本文主要以演示为主,主要是让大家熟悉封装自定义错误的思路,故而封装的较为简单。大家可根据自己公司需求来进行拓展。
代码仓库地址:https://github.com/ziyifast/ziyifast-code_instruction
- 重写控制台打印格式
- 封装new方法
- DefaultBizWrap:不含原始err
- BizWrap:包含原始err
package zerr
import (
"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
}
fallthrough
case '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
}
fallthrough
case '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 error
msg 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
}
fallthrough
case '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 error
code string
vars []string
}
func (w *ErrWrap) Vars() []string {
return w.vars
}
func (w *ErrWrap) Code() string {
return w.code
}
func (w *ErrWrap) Error() string {
var msg string
if 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
}
fallthrough
case '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(),
}
}
定义堆栈打印格式
package zerr
import (
"fmt"
"io"
"path"
"runtime"
"strings"
)
type Frame uintptr
func (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 []Frame
func (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 []uintptr
func (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 = 32
var pcs [depth]uintptr
n := 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:]
}
由自定义err,判断是否属于某个err
package zerr
import (
"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()
自定义业务错误码、对应错误信息及对应错误对应的httpStatusCode
package biz_err
import (
"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 error
if as := zerr.As(err, &errWrap); as {
code = errWrap.Code()
cause = errWrap.Cause()
vars = errWrap.Vars()
} else {
code = Undefined
}
if code == Undefined {
var undefinedMsg string
if 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 = bizMsg
if 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 error
if 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 = bizMsg
if cause != nil {
_, _, causeMsg := ErrResponse(cause)
if causeMsg != "" {
msg += causeMsg
} else {
msg += errWrap.Error()
}
}
} else {
msg = errWrap.Error()
}
return httpStatus, code, msg
}
package constant
const (
ContentTypeJson = "application/json"
ContentTypeXml = "application/xml"
)
package controller
import (
"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)
}
package controller
import (
"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)
}
package response
import (
"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 string
type 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,
}
}
package main
import (
"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)
}
package main
import (
"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信息