系統在運行時發生問題,最直觀的作法就是直接把這些error回傳,如果這是使用者不當輸入的錯誤還沒什麼問題,但如果是系統底層的bug,直接回傳到client可能會有資料疑慮,所以最好寫一套錯誤處理辦法,規範要給client的訊息,以及提供我們日後debug的資訊。
先來定義一些error種類,使用error code來map錯誤類別,這邊我稍微進行分組
在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
有需要隨時可以添加其他的錯誤類別
進入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
}
}
解釋:
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