iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 4
0

錯誤處理

系統在運行時發生問題,最直觀的作法就是直接把這些error回傳,如果這是使用者不當輸入的錯誤還沒什麼問題,但如果是系統底層的bug,直接回傳到client可能會有資料疑慮,所以最好寫一套錯誤處理辦法,規範要給client的訊息,以及提供我們日後debug的資訊。

先來定義一些error種類,使用error code來map錯誤類別,這邊我稍微進行分組

  1. 第一位: 粗略區分系統錯誤(後端的鍋)與應用程式的錯誤(例如不當的輸入)
  • 1: 系統錯誤
  • 2: 應用錯誤
  1. 第二到四位: 回應client的http status
  2. 第五位以後:單純編號

在config/app下建立error.yaml,在裡面寫入一些設定

-1: Ok
0: Unknown

# 系统級错误
1500001: System error
1500002: Service unavailable
1500003: Connection error
1500004: Wrong error type
1500005: Wrong logger type
1500006: database error

# 應用級錯誤
2400000: Application error
2400001: Parameter error
2400002: Permission denied

有需要隨時可以添加其他的錯誤類別

code

進入app打開setting/setting.go,把這些設定讀進來

寫入

type ErrorMapStruct map[int]string

在var補上

ErrorMap ErrorMapStruct

接著在init內補上

	// read apperr config file
	config, err = ioutil.ReadFile(WorkPath + "error.yaml")
	if err != nil {
		panic(err)
	}

	// binding
	ErrorMap = make(map[int]string)
	err = yaml.Unmarshal(config, &ErrorMap)
	if err != nil {
		panic(err)
	}

創建apperr package,在底下創建code.go補上

package apperr

const (
	ErrSystemFail       = 1500001
	ErrConnectFail      = 1500003
	ErrDatabaseFail     = 1500006
	ErrWrongArgument    = 2400001
	ErrPermissionDenied = 2400002
)

創建error.go寫入

package apperr

import (
	"app/setting"
)

// check if error code exist
func GetMsg(code int) string {
	if val, ok := setting.ErrorMap[code]; ok {
		return val
	} else {
		return setting.ErrorMap[0]
	}
}

// split error code
// return errorType, httpStatus, customCode
func SplitCode(code int) (int, int, int) {
	if _, ok := setting.ErrorMap[code]; !ok {
		//error code not found
		return 0, 0, 0
	}

	return code / 1000000, (code % 1000000) / 1000, code % 1000
}

發生錯誤(panic)時,我們需要得到stack的訊息來幫助debug,可以依靠go的runtime包來得到這些資訊

創建util資料夾用來放可以獨立於這專案外的一些函式,在底下創建debug package,建立stack.go寫入

package debug

import (
	"runtime"
	"strconv"
)

const WrapLv = 0

type FuncDataStruct struct {
	Loacate  string
	Function string
}

// get the function data of caller, start from 0
func GetFuncData(skipNum ...int) *FuncDataStruct {
	skip := getWrapLv(skipNum...)
	pc, file, line, ok := runtime.Caller(skip + 1) // add one to skip this function
	if !ok {
		return nil
	}

	return &FuncDataStruct{
		Loacate:  file + ":" + strconv.Itoa(line),
		Function: runtime.FuncForPC(pc).Name(),
	}
}

// stack returns a nicely formatted stack frame, skipping skip frames.
func GetCallStack(skipNum ...int) []*FuncDataStruct {
	// check skip number
	skip := getWrapLv(skipNum...)

	var stack []*FuncDataStruct

	for i := skip + 1; ; i++ { // Skip the expected number of frames, add 1 to skip this function
		funcData := GetFuncData(i)
		if funcData == nil {
			break
		}

		stack = append(stack, funcData)
	}

	//skip runtime.main and exist
	return stack[:len(stack)-2]
}

func getWrapLv(skipNum ...int) int {
	// check skip number
	if len(skipNum) > 0 {
		return skipNum[0] + WrapLv
	} else {
		return WrapLv
	}
}

解釋:

  • FuncDataStruct定義了所屬的檔案,函數名稱與第幾行
  • skipNum 代表跳過runtime.Caller回傳的幾層stack,0代表當前的函數資訊,因為有GetFuncData的包裝,需要多加1來略過這個函數
  • return stack[:len(stack) - 2]runtime.Caller的最後兩層是runtime底層的函數,跟我們需要的資訊無關,所以跳過

寫個test看看,創建stack_test.go,寫入

package apperr

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 errorData.Msg == setting.ErrorMap[0] {
			if err != nil {
				errorData.Msg = err.Error()
			}
		}*/
	if len(customMsg) > 1 {
		errorMeta = NewErrorMeta(err, stack, customMsg[1])
	} else {
		errorMeta = NewErrorMeta(err, stack, "")
	}

	return &ErrorReturn{errorData, errorMeta}
}

// check if error code exist
func GetMsg(code int) string {
	if val, ok := setting.ErrorMap[code]; ok {
		return val
	} else {
		return setting.ErrorMap[0]
	}
}

// split error code
// return errorType, httpStatus, customCode
func SplitCode(code int) (int, int, int) {
	if _, ok := setting.ErrorMap[code]; !ok {
		//error code not found
		return 0, 0, 0
	}

	return code / 1000000, (code % 1000000) / 1000, code % 1000
}

跑跑看

go test ./util/debug

ok app/util/debug 0.001s

總結

今天把基礎需要的變數與函數完成,明天來寫middleware

目前的工作環境

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

上一篇
Day3 URL處理
下一篇
Day5 錯誤處理(下)
系列文
從coding到上線-打造自己的blog系統30

尚未有邦友留言

立即登入留言