iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
0
Modern Web

從coding到上線-打造自己的blog系統系列 第 5

Day5 錯誤處理(下)

昨天把基礎要用的函數與變數搞定了,今天來把錯誤處理完成。

現在定義兩種struct ErrorData與ErrorMeta,分別代表對外表示的錯誤訊息,與原始用於debug的錯誤資訊。

在error.go寫入

import (
	"app/setting"
	"app/util/debug"
)

// struct of error message to let out
type ErrorDataStruct struct {
	Code int    `zap:"code"`
	Msg  string `zap:"msg"`
}

// error message for debug
type ErrorMetaStruct struct {
	Msg   string
	Error error // origin error
	Stack []*debug.FuncDataStruct
}

// return error for logger
type ErrorReturn struct {
	ErrorData *ErrorDataStruct
	ErrorMeta *ErrorMetaStruct
}

func (er *ErrorReturn) Error() string {
	return er.ErrorData.Msg
}

// implement Unwrap for error in go 1.13
func (er *ErrorReturn) Unwrap() error {
	return er.ErrorMeta.Error
}

func (ems *ErrorMetaStruct) GetStack() []*debug.FuncDataStruct {
	return ems.Stack
}

// implement Unwrap for error in go 1.13
func (ems *ErrorMetaStruct) Unwrap() error {
	return ems.Error
}

//generate new error data
func NewErrorData(code int, customMsg string) (eData *ErrorDataStruct) {
	if customMsg != "" {
		eData = &ErrorDataStruct{
			Code: code,
			Msg:  customMsg,
		}
	} else {
		eData = &ErrorDataStruct{
			Code: code,
			Msg:  GetMsg(code),
		}
	}
	return
}

//generate new error meta
func NewErrorMeta(err error, stack []*debug.FuncDataStruct, customMsg string) *ErrorMetaStruct {
	return &ErrorMetaStruct{
		Msg:   customMsg,
		Error: err,
		Stack: stack,
	}
}

// new a error return struct
func NewErrorReturn(code int, err error, stack []*debug.FuncDataStruct, customMsg ...string) *ErrorReturn {
	var (
		errorData *ErrorDataStruct
		errorMeta *ErrorMetaStruct
	)

	// check error msg
	if len(customMsg) > 0 {
		errorData = NewErrorData(code, customMsg[0])
	} else {
		errorData = NewErrorData(code, "")
	}

	if len(customMsg) > 1 {
		errorMeta = NewErrorMeta(err, stack, customMsg[1])
	} else {
		errorMeta = NewErrorMeta(err, stack, "")
	}

	return &ErrorReturn{errorData, errorMeta}
}

解釋:

  • Unwrap是go 1.13對錯誤處理的改進,常常可以看到go的一些package有自定義的error擴展型態(畢竟只有一個string真的很難用...),對於error的擴展類型定義Unwrap讓我們可以方便的嵌套error,詳情可以看官方說明

創建handle.go處理error,寫入

package apperr

import (
	"app/setting"
	"app/util/debug"
	"errors"
	"github.com/gin-gonic/gin"
)

// return wrap error type
func New(code int, err error, skip int, customMsg ...string) (er error) {
	// NewErrorReturn
	if setting.Servers["main"].RunMode == gin.DebugMode {
		er = NewErrorReturn(code, err, debug.GetCallStack(skip+1), customMsg...) // skip this level
	} else {
		er = NewErrorReturn(code, err, nil, customMsg...)
	}

	return
}

// set context.error to handle to front end
// skip will be set for stack level(start from 0)
func ErrorHandle(c *gin.Context, code int, err error, skip int, customMsg ...string) (er *ErrorReturn) {
	er = New(code, err, skip, customMsg...).(*ErrorReturn)
	_ = c.Error(errors.New("")).SetMeta(er.ErrorData)
	return
}

解釋:

  • 區分開debug與release mode,release不需要stack資訊
  • c.error是gin使用的錯誤處理方法,SetMeta可以將自訂的錯誤資訊存在gin context裡,我們等等在middleware取出

現在來寫middleware處理error,如果在運行中發生panic,我們需要recover來避免整個系統掛掉,gin 本身就有提供recovery的middleware可以使用,這邊我直接修改源碼統一把recover與錯誤處理一併解決。

創建middleware package,創建error.go寫入

package middleware

import (
	"app/apperr"
	"app/logger"
	"github.com/gin-gonic/gin"
	"net"
	"os"
	"strings"
)

// ErrorHandling returns a middleware that recovers from any panics and writes a 500 if there was one
// if no panic but there is error in context error, handle for warning to cleint side
func ErrorHandle() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			var brokenPipe bool
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				// detect error type
				if brokenPipe {
					// connect error, can't send to front
					log.Error(c, apperr.ErrConnectFail, err.(error), 1)
				} else {
					// system error, need to send to front
					log.Error(c, apperr.ErrPermissionDenied, err.(error), 1, "Sorry, something apperr")
				}

				c.Abort()
			}

			// if no error
			err := c.Errors.Last()
			if err == nil {
				// logger success
				log.Success(c)
				return
			}

			// If the connection is dead, we can't write a status to it.
			if !brokenPipe {
				// get apperr meta
				var (
					code int
					msg  string
				)
				switch err.Meta.(type) {
				case *apperr.ErrorDataStruct:
					meta := err.Meta.(*apperr.ErrorDataStruct)
					code = meta.Code
					msg = meta.Msg
				default:
					// worng type or something error
					code = 1500004
					msg = "Sorry, Something error"
					log.Warn(c, code, nil, msg)
				}

				// return to client
				_, httpStatus, _ := apperr.SplitCode(code)
				c.JSON(httpStatus, gin.H{
					"Code": code,
					"Msg":  msg,
				})
			}
		}()
		c.Next()
	}
}

解釋:

  • log.Error, log.Warn, log.Success在下一篇會提到,簡單來說是以log紀錄資訊,會呼叫error.ErrorHandle
  • brokenPipe用來確認連接錯誤,是gin recovery本來就有的部份,連接錯誤時沒辦法傳送給client

在router.main,找到r := gin.New(),在下面補上

r.Use(middleware.ErrorHandle())

這樣就能使用自己寫的error middleware了

總結

目前我們只有處理給client的訊息還沒處理後段自己要看的錯誤資訊,這會在下一篇提到。

目前的專案結構

.
├── app
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── config
│   │   └── app
│   │       ├── app.yaml
│   │       └── error.yaml
│   ├── error
│   │   ├── code.go
│   │   ├── error.go
│   │   └── handle.go
│   ├── middleware
│   │   └── error.go
│   ├── router
│   │   ├── host_switch.go
│   │   └── main.go
│   ├── serve
│   │   ├── main.go
│   │   └── main_test.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── debug
│           ├── stack.go
│           └── stack_test.go
└── database

上一篇
Day4 錯誤處理(上)
下一篇
Day6 Log處理(上)
系列文
從coding到上線-打造自己的blog系統31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言