欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

go語言規(guī)范RESTful?API業(yè)務(wù)錯誤處理

 更新時間:2023年03月08日 08:56:32   作者:江湖十年  
這篇文章主要為大家介紹了go語言規(guī)范RESTful?API業(yè)務(wù)錯誤處理方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

錯誤碼

現(xiàn)如今,主流的 Web API 都采用 RESTful 設(shè)計風(fēng)格,對于接口返回的 HTTP 狀態(tài)碼和響應(yīng)內(nèi)容都有統(tǒng)一的規(guī)范。針對接口錯誤響應(yīng),一般都會返回一個 Code(錯誤碼)和 Message(錯誤消息內(nèi)容),通常錯誤碼 Code 用來定位一個唯一的錯誤,錯誤消息 Message 用來展示錯誤信息。

本文就來詳細(xì)介紹下,如何將 RESTful API 的錯誤處理進行規(guī)范化。

為什么需要業(yè)務(wù)錯誤碼

雖然 RESTful API 能夠通過 HTTP 狀態(tài)碼來標(biāo)記一個請求的成功或失敗,但 HTTP 狀態(tài)碼作為一個通用的標(biāo)準(zhǔn),并不能很好的表達業(yè)務(wù)錯誤。

比如一個 500 的錯誤響應(yīng),可能是由后端數(shù)據(jù)庫連接異常引起的、也可能由內(nèi)部代碼邏輯錯誤引起,這些都無法通過 HTTP 狀態(tài)碼感知到,如果程序出現(xiàn)錯誤,不方便開發(fā)人員 Debug。

因此我們有必要設(shè)計一套用來標(biāo)識業(yè)務(wù)錯誤的錯誤碼,這有別于 HTTP 狀態(tài)碼,是跟系統(tǒng)具體業(yè)務(wù)息息相關(guān)的。

錯誤碼功能

在設(shè)計錯誤碼之前,我們需要明確下錯誤碼應(yīng)該具備哪些屬性,以滿足業(yè)務(wù)需要。

  • 錯誤碼必須是唯一的。只有錯誤碼是唯一的才方便在程序出錯時快速定位問題,不然程序出錯,返回錯誤碼不唯一,想要根據(jù)錯誤碼排查問題,就要針對這一錯誤碼所表示的錯誤列表進行逐一排查。
  • 錯誤碼需要是可閱讀的。意思是說,通過錯誤碼,我們就能快速定位到是系統(tǒng)的哪個組件出現(xiàn)了錯誤,并且知道錯誤的類型,不然也談不上叫「業(yè)務(wù)錯誤碼」了。一個清晰可讀的錯誤碼在微服務(wù)系統(tǒng)中定位問題尤其有效。
  • 通過錯誤碼能夠方便知道 HTTP 狀態(tài)碼。這一點往往容易被人忽略,不過我比較推薦這種做法,因為在 Review 代碼時,通過返回錯誤碼,就能很容易知道接口返回 HTTP 狀態(tài)碼,這不僅方便理解代碼,更方便錯誤的統(tǒng)一處理。

錯誤碼設(shè)計

錯誤碼調(diào)研

錯誤碼的設(shè)計我們可以參考業(yè)內(nèi)使用量比較大的開放 API 設(shè)計,比較有代表性的是阿里云和新浪網(wǎng)的開放 API。

如以下是一個阿里云 ECS 接口錯誤的返回:

{
	"RequestId": "5E571499-13C5-55E3-9EA6-DEFA0DBC85E4",
	"HostId": "ecs-cn-hangzhou.aliyuncs.com",
	"Code": "InvalidOperation.NotSupportedEndpoint",
	"Message": "The specified endpoint can't operate this region. Please use API DescribeRegions to get the appropriate endpoint, or upgrade your SDK to latest version.",
	"Recommend": "https://next.api.aliyun.com/troubleshoot?q=InvalidOperation.NotSupportedEndpoint&product=Ecs"
}

可以發(fā)現(xiàn),Code 和 Message 都為字符串類型,并且還有 RequestId(當(dāng)前請求唯一標(biāo)識)、HostId(Host 唯一標(biāo)識)、Recommend(錯誤診斷地址),可以說這個錯誤信息非常全面了。

再來看下新浪網(wǎng)開放 API 錯誤返回結(jié)果的設(shè)計:

{
	"request": "/statuses/home_timeline.json",
	"error_code": "20502",
	"error": "Need you follow uid."
}

相比阿里云,新浪網(wǎng)的錯誤返回更簡潔一些。其中 request 為請求路徑,error_code 即為錯誤碼 Code,error 則表示錯誤信息 Message。

錯誤代碼 20502 說明如下:

20502
服務(wù)級錯誤(1為系統(tǒng)級錯誤)服務(wù)模塊代碼具體錯誤代碼

新浪網(wǎng)的錯誤碼為數(shù)字類型的字符串,相比阿里云的錯誤碼要簡短不少,并且對程序更加友好,也是我個人更推薦的設(shè)計。

業(yè)務(wù)錯誤碼

結(jié)合市面上這些優(yōu)秀的開放 API 錯誤碼設(shè)計,以及我在實際開發(fā)中的工作總結(jié),我設(shè)計的錯誤碼規(guī)則如下:

業(yè)務(wù)錯誤碼由 8 位純數(shù)字組成,類型為 int。

業(yè)務(wù)錯誤碼示例格式:40001002

錯誤碼說明:

1-3 位4-5 位6-8 位
40001002
HTTP 狀態(tài)碼組件編號組件內(nèi)部錯誤碼

錯誤碼設(shè)計為純數(shù)字主要是為了程序中使用起來更加方便,比如根據(jù)錯誤碼計算 HTTP 狀態(tài)碼,只需要通過簡單的數(shù)學(xué)取模計算就能做到。

使用兩位數(shù)字來標(biāo)記不同組件,最多能表示 99 個組件,即使項目全部采用微服務(wù)開發(fā),一般來說也是足夠用的。

最后三位代表組件內(nèi)部錯誤碼,最多能表示 1000 個錯誤。其實通常來說一個組件內(nèi)部是用不上這么多錯誤的,如果組件較小,完全可以設(shè)計成兩位數(shù)字。

另外,有些廠商中還會設(shè)計一些公共的錯誤碼,可以稱為「全局錯誤碼」,這些錯誤碼在各組件間通用,以此來減少定義重復(fù)錯誤。在我們的錯誤碼設(shè)計中,可以將組件編號為 00 的標(biāo)記為全局錯誤碼,其他組件編號從 01 開始。

錯誤格式

有了錯誤碼,還需要定義錯誤響應(yīng)格式,設(shè)計一個標(biāo)準(zhǔn)的 API 錯誤響應(yīng)格式如下:

{
	"code": 50000000,
	"message": "系統(tǒng)錯誤",
	"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}

code 即為錯誤碼,message 為錯誤信息,reference 則是錯誤文檔地址,用來告知用戶如何解決這個錯誤,對標(biāo)的是阿里云錯誤響應(yīng)中的 Recommend 字段。

錯誤碼實現(xiàn)

因為每一個錯誤碼和錯誤信息以及錯誤文檔地址都是一一對應(yīng)的,所以我們需要一個對象來保存這些信息,在 Go 中可以使用結(jié)構(gòu)體。

可以設(shè)計如下結(jié)構(gòu)體:

type apiCode struct {
	code int
	msg  string
	ref  string
}

這是一個私有結(jié)構(gòu)體,外部項目要想使用,則需要一個構(gòu)造函數(shù):

func NewAPICode(code int, message string, reference ...string) APICoder {
	ref := ""
	if len(reference) > 0 {
		ref = reference[0]
	}
	return &apiCode{
		code: code,
		msg:  message,
		ref:  ref,
	}
}

其中 reference 被設(shè)計為可變參數(shù),如果不傳則默認(rèn)為空。

NewAPICode 返回值 APICoder 是一個接口,這在 Go 中是一種慣用做法。通過接口可以解耦,方便依賴 apiCode 的代碼編寫測試,用戶可以對 APICoder 進行 Mock;另一方面,我們稍后會為 apiCode 實現(xiàn)對應(yīng)的錯誤包,使用接口來表示錯誤碼可以方便用戶定義自己的 apiCode 類型。

為了便于使用,apiCode 提供了如下幾個能力:

func (a *apiCode) Code() int {
	return a.code
}
func (a *apiCode) Message() string {
	return a.msg
}
func (a *apiCode) Reference() string {
	return a.ref
}
func (a *apiCode) HTTPStatus() int {
	v := a.Code()
	for v >= 1000 {
		v /= 10
	}
	return v
}

至此 APICoder 接口接口的定義也就有了:

type APICoder interface {
	Code() int
	Message() string
	Reference() string
	HTTPStatus() int
}

apiCode 則實現(xiàn)了 APICoder 接口。

現(xiàn)在我們可以通過如下方式創(chuàng)建錯誤碼結(jié)構(gòu)體對象:

var (
	CodeBadRequest   = NewAPICode(40001001, "請求不合法")
	CodeUnknownError = NewAPICode(50001001, "系統(tǒng)錯誤", "https://github.com/jianghushinian/gokit/tree/main/errors")
)

錯誤包

設(shè)計好了錯誤碼,并不能直接使用,我們還需要一個與之配套的錯誤包來簡化錯誤碼的使用。

錯誤包功能

錯誤包要能夠完美支持上面設(shè)計的錯誤碼。所以需要使用 APICoder 來構(gòu)造錯誤對象。

錯誤包應(yīng)該能夠查看原始錯誤原因。這就需要實現(xiàn) Unwrap 方法,Wrap/Unwrap 方法是在 Go 1.13 中被加入進 errors 包的,目的是能夠處理嵌套錯誤。

錯誤包應(yīng)該能夠支持對內(nèi)對外展示不同信息。這就需要實現(xiàn) Format 方法,根據(jù)需要可以將錯誤格式化成不同輸出。

錯誤包應(yīng)該能夠支持展示堆棧信息。這對 Debug 來說相當(dāng)重要,也是 Go 自帶的 errors 包不足的地方。

為了方便在日志中記錄結(jié)構(gòu)化錯誤信息,錯誤包還要能夠支持 JSON 序列化。這需要實現(xiàn) MarshalJSON/UnmarshalJSON 兩個方法。

錯誤包設(shè)計

一個錯誤對象結(jié)構(gòu)體設(shè)計如下:

type apiError struct {
	coder APICoder
	cause error
	*stack
}

其中 coder 用來保存實現(xiàn)了 APICoder 接口的對象,cause 用來記錄錯誤原因,stack 用來展示錯誤堆棧。

錯誤對象的構(gòu)造函數(shù)如下:

var WrapC = NewAPIError
func NewAPIError(coder APICoder, cause ...error) error {
	var c error
	if len(cause) > 0 {
		c = cause[0]
	}
	return &apiError{
		coder: coder,
		cause: c,
		stack: callers(),
	}
}

NewAPIError 通過 APICoder 來創(chuàng)建錯誤對象,第二個參數(shù)為一個可選的錯誤原因。

其實構(gòu)造一個錯誤對象也就是對一個錯誤進行 Wrap 的過程,所以我還為構(gòu)造函數(shù) NewAPIError 定義了一個別名 WrapC,表示使用錯誤碼將一個錯誤包裝成一個新的錯誤。

一個錯誤對象必須要實現(xiàn) Error 方法:

func (a *apiError) Error() string {
	return fmt.Sprintf("[%d] - %s", a.coder.Code(), a.coder.Message())
}

默認(rèn)情況下,獲取到的錯誤內(nèi)容只包含錯誤碼 Code 和錯誤信息 Message。

為了方便獲取被包裝錯誤的原始錯誤,還要實現(xiàn) Unwrap 方法:

func (a *apiError) Unwrap() error {
	return a.cause
}

為了能在打印或?qū)懭肴罩緯r展示不同信息,則要實現(xiàn) Format 方法:

func (a *apiError) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			str := a.Error()
			if a.Unwrap() != nil {
				str += " " + a.Unwrap().Error()
			}
			_, _ = io.WriteString(s, str)
			a.stack.Format(s, verb)
			return
		}
		if s.Flag('#') {
			cause := ""
			if a.Unwrap() != nil {
				cause = a.Unwrap().Error()
			}
			data, _ := json.Marshal(errorMessage{
				Code:      a.coder.Code(),
				Message:   a.coder.Message(),
				Reference: a.coder.Reference(),
				Cause:     cause,
				Stack:     fmt.Sprintf("%+v", a.stack),
			})
			_, _ = io.WriteString(s, string(data))
			return
		}
		fallthrough
	case 's':
		_, _ = io.WriteString(s, a.Error())
	case 'q':
		_, _ = fmt.Fprintf(s, "%q", a.Error())
	}
}

Format 方法能夠支持在使用 fmt.Printf("%s", apiError) 格式化輸出時打印定制化的信息。

Format 方法支持的不同格式輸出如下:

格式占位符輸出信息
%s錯誤碼、錯誤信息
%v錯誤碼、錯誤信息,與 %s 等價
%+v錯誤碼、錯誤信息、錯誤原因、錯誤堆棧
%#vJSON 格式的 錯誤碼、錯誤信息、錯誤文檔地址、錯誤原因、錯誤堆棧
%q在 錯誤碼、錯誤信息 外層增加了一個雙引號

這些錯誤格式基本上能滿足所有業(yè)務(wù)開發(fā)中的需求了,如果還有其他格式需要,則可以在此基礎(chǔ)上進一步開發(fā) Format 方法。

用來進行 JSON 序列化和反序列化的 MarshalJSON/UnmarshalJSON 方法實現(xiàn)如下:

func (a *apiError) MarshalJSON() ([]byte, error) {
	return json.Marshal(&errorMessage{
		Code:      a.coder.Code(),
		Message:   a.coder.Message(),
		Reference: a.coder.Reference(),
	})
}
func (a *apiError) UnmarshalJSON(data []byte) error {
	e := &errorMessage{}
	if err := json.Unmarshal(data, e); err != nil {
		return err
	}
	a.coder = NewAPICode(e.Code, e.Message, e.Reference)
	return nil
}
type errorMessage struct {
	Code      int    `json:"code"`
	Message   string `json:"message"`
	Reference string `json:"reference,omitempty"`
	Cause     string `json:"cause,omitempty"`
	Stack     string `json:"stack,omitempty"`
}

為了不對外部暴露敏感信息,對外的 HTTP API 只會返回 Code、MessageReference(可選)三個字段,對內(nèi)需要額外展示錯誤原因以及錯誤堆棧。所以 errorMessageReferenceCause、Stack 字段都帶有 omitempty 屬性,這樣在 MarshalJSON 時只會序列化 CodeMessage、Reference 這三個字段。

至此,我們就實現(xiàn)了錯誤包的設(shè)計。

錯誤碼及錯誤包的使用

使用示例

通過上面的講解,我們了解了錯誤碼和錯誤包的設(shè)計規(guī)范,接下來看看如何使用它們。這里以錯誤碼及錯誤包在 Gin 中的使用為例進行講解。

使用 Gin 構(gòu)建一個簡單的 Web Server 如下:

package main
import (
	"errors"
	"fmt"
	"strconv"
	"github.com/gin-gonic/gin"
	apierr "github.com/jianghushinian/gokit/errors"
)
var (
	ErrAccountNotFound = errors.New("account not found")
	ErrDatabase        = errors.New("database error")
)
var (
	CodeBadRequest   = NewAPICode(40001001, "請求不合法")
	CodeNotFound     = NewAPICode(40401001, "資源未找到")
	CodeUnknownError = NewAPICode(50001001, "系統(tǒng)錯誤", "https://github.com/jianghushinian/gokit/tree/main/errors")
)
type Account struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}
func AccountOne(id int) (*Account, error) {
	for _, v := range accounts {
		if id == v.ID {
			return &v, nil
		}
	}
	// 模擬返回數(shù)據(jù)庫錯誤
	if id == 500 {
		return nil, ErrDatabase
	}
	return nil, ErrAccountNotFound
}
var accounts = []Account{
	{ID: 1, Name: "account_1"},
	{ID: 2, Name: "account_2"},
	{ID: 3, Name: "account_3"},
}
func ShowAccount(c *gin.Context) {
	id := c.Param("id")
	aid, err := strconv.Atoi(id)
	if err != nil {
		// 將 errors 包裝成 APIError 返回
		ResponseError(c, apierr.WrapC(CodeBadRequest, err))
		return
	}
	account, err := AccountOne(aid)
	if err != nil {
		switch {
		case errors.Is(err, ErrAccountNotFound):
			err = apierr.NewAPIError(CodeNotFound, err)
		case errors.Is(err, ErrDatabase):
			err = apierr.NewAPIError(CodeUnknownError, fmt.Errorf("account %d: %w", aid, err))
		}
		ResponseError(c, err)
		return
	}
	ResponseOK(c, account)
}
func main() {
	r := gin.Default()
	r.GET("/accounts/:id", ShowAccount)
	if err := r.Run(":8080"); err != nil {
		panic(err)
	}
}

在這個 Web Server 中定義了一個 ShowAccount 函數(shù),用來處理獲取賬號邏輯,在 ShowAccount 內(nèi)部程序執(zhí)行成功返回 ResponseOK(c, account),失敗則返回 ResponseError(c, err)

在處理返回失敗的響應(yīng)時,都會通過 apierr.WrapCapierr.NewAPIError 將底層函數(shù)返回的初始錯誤進行一層包裝,根據(jù)錯誤級別,包裝成不同的錯誤碼進行返回。

其中 ResponseOKResponseError 定義如下:

func ResponseOK(c *gin.Context, spec interface{}) {
	if spec == nil {
		c.Status(http.StatusNoContent)
		return
	}
	c.JSON(http.StatusOK, spec)
}
func ResponseError(c *gin.Context, err error) {
	log(err)
	e := apierr.ParseCoder(err)
	httpStatus := e.HTTPStatus()
	if httpStatus >= 500 {
		// send error msg to email/feishu/sentry...
		go fakeSendErrorEmail(err)
	}
	c.AbortWithStatusJSON(httpStatus, err)
}
// log 打印錯誤日志,輸出堆棧
func log(err error) {
	fmt.Println("========== log start ==========")
	fmt.Printf("%+v\n", err)
	fmt.Println("========== log end ==========")
}
// fakeSendErrorEmail 模擬將錯誤信息發(fā)送到郵件,JSON 格式
func fakeSendErrorEmail(err error) {
	fmt.Println("========== error start ==========")
	fmt.Printf("%#v\n", err)
	fmt.Println("========== error end ==========")
}

ResponseOK 其實就是 Gin 框架的正常返回,ResponseError 則專門用來處理并返回 API 錯誤。

ResponseError 中首先通過 log(err) 來記錄錯誤日志,在其內(nèi)部使用 fmt.Printf("%+v\n", err) 進行打印。

之后我們還對 HTTP 狀態(tài)碼進行了判斷,大于 500 的錯誤將會發(fā)送郵件通知,這里使用 fmt.Printf("%#v\n", err) 進行模擬。

其中 apierr.ParseCoder(err) 能夠從一個錯誤對象中獲取到實現(xiàn)了 APICoder 的錯誤碼對象,實現(xiàn)如下:

func ParseCoder(err error) APICoder {
	for {
		if e, ok := err.(interface {
			Coder() APICoder
		}); ok {
			return e.Coder()
		}
		if errors.Unwrap(err) == nil {
			return CodeUnknownError
		}
		err = errors.Unwrap(err)
	}
}

這樣,我們就能夠通過一個簡單的 Web Server 示例程序來演示如何使用錯誤碼和錯誤包了。

可以通過 go run main.go 啟動這個 Web Server。

先來看下在這個 Web Server 中一個正常的返回結(jié)果是什么樣,使用 cURL 來發(fā)送一個請求:curl http://localhost:8080/accounts/1

客戶端得到如下響應(yīng)結(jié)果:

{
	"id": 1,
	"name": "account_1"
}

服務(wù)端打印正常的請求日志:

再來測試下請求一個不存在的賬號:curl http://localhost:8080/accounts/12。

客戶端得到如下響應(yīng)結(jié)果:

{
	"code": 40401001,
	"message": "資源未找到"
}

返回結(jié)果中沒有 reference 字段,是因為對于 reference 為空的情況,在 JSON 序列化過程中會被隱藏。

服務(wù)端打印的錯誤日志如下:

========== log start ==========
[40401001] - 資源未找到 account not found
main.ShowAccount
        /app/errors/examples/main.go:56
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.LoggerWithConfig.func1
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620
github.com/gin-gonic/gin.(*Engine).ServeHTTP
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576
net/http.serverHandler.ServeHTTP
        /usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
        /usr/local/go/src/net/http/server.go:1991
runtime.goexit
        /usr/local/go/src/runtime/asm_arm64.s:1165
========== log end ==========

可以發(fā)現(xiàn),錯誤日志中不僅打印了錯誤碼([40401001])和錯誤信息(資源未找到),還打印了錯誤原因(account not found)以及下面的錯誤堆棧。

如此清晰的錯誤日志得益于我們實現(xiàn)的 Format 函數(shù)的強大功能。

現(xiàn)在再來觸發(fā)一個 HTTP 狀態(tài)碼為 500 的錯誤響應(yīng):curl http://localhost:8080/accounts/500。

客戶端得到如下響應(yīng)結(jié)果:

{
	"code": 50001001,
	"message": "系統(tǒng)錯誤",
	"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}

這次得到一個帶有 reference 字段的完整錯誤響應(yīng)。

服務(wù)端打印的錯誤日志如下:

========== log start ==========
[50001001] - 系統(tǒng)錯誤 account 500: database error
main.ShowAccount
        /app/errors/examples/main.go:58
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.LoggerWithConfig.func1
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240
github.com/gin-gonic/gin.(*Context).Next
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174
github.com/gin-gonic/gin.(*Engine).handleHTTPRequest
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620
github.com/gin-gonic/gin.(*Engine).ServeHTTP
        /go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576
net/http.serverHandler.ServeHTTP
        /usr/local/go/src/net/http/server.go:2947
net/http.(*conn).serve
        /usr/local/go/src/net/http/server.go:1991
runtime.goexit
        /usr/local/go/src/runtime/asm_arm64.s:1165
========== log end ==========
[GIN] 2023/03/05 - 02:02:28 | 500 |     426.292µs |       127.0.0.1 | GET      "/accounts/500"
========== error start ==========
{"code":50001001,"message":"系統(tǒng)錯誤","reference":"https://github.com/jianghushinian/gokit/tree/main/errors","cause":"account 500: database error","stack":"\nmain.ShowAccount\n\t/app/errors/examples/main.go:58\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.CustomRecoveryWithWriter.func1\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/recovery.go:102\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.LoggerWithConfig.func1\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/logger.go:240\ngithub.com/gin-gonic/gin.(*Context).Next\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/context.go:174\ngithub.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:620\ngithub.com/gin-gonic/gin.(*Engine).ServeHTTP\n\t/go/pkg/mod/github.com/gin-gonic/gin@v1.9.0/gin.go:576\nnet/http.serverHandler.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2947\nnet/http.(*conn).serve\n\t/usr/local/go/src/net/http/server.go:1991\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_arm64.s:1165"}
========== error end ==========

這一次除了 log 函數(shù)打印的日志,還能看到 fakeSendErrorEmail 函數(shù)打印的日志,正是一個 JSON 格式的結(jié)構(gòu)化日志。

以上便是我們設(shè)計的錯誤碼及錯誤包在實際開發(fā)場景中的應(yīng)用。

使用建議

根據(jù)我的經(jīng)驗,總結(jié)了一些錯誤碼及錯誤包的使用建議,現(xiàn)在將其分享給你。

使用盡量少的 HTTP 狀態(tài)碼

HTTP 狀態(tài)碼大概分為 5 大類,分別是 1XX、2XX、3XX、4XX、5XX。根據(jù)我的實際工作經(jīng)驗,我們并不會使用全部的狀態(tài)碼,最常用的狀態(tài)碼不超過 10 個。

所以即使我們設(shè)計的業(yè)務(wù)錯誤碼支持?jǐn)y帶 HTTP 狀態(tài)碼,但也不推薦使用過多的 HTTP 狀態(tài)碼,以免加重前端工作量。

推薦在錯誤碼中使用的 HTTP 狀態(tài)碼如下:

  • 400: 請求不合法
  • 401: 認(rèn)證失敗
  • 403: 授權(quán)失敗
  • 404: 資源未找到
  • 500: 系統(tǒng)錯誤

其中 4XX 代表客戶端錯誤,而如果是服務(wù)端錯誤,則統(tǒng)一使用 500 狀態(tài)碼,具體錯誤原因可以通過業(yè)務(wù)錯誤碼定位。

使用中間件來記錄錯誤日志

由于我們設(shè)計的錯誤包支持 Unwrap 操作,所以建議出現(xiàn)錯誤時的處理流程如下:

  • 最底層代碼遇到錯誤時通過 errors.New/fmt.Errorf 來創(chuàng)建一個錯誤對象,然后將錯誤返回(可選擇性的記錄一條日志)。
func Query(id int) (obj, error) {
    // do something
    return nil, fmt.Errorf("%d not found", id)
}
  • 中間過程中處理函數(shù)遇到下層函數(shù)返回的錯誤,不做任何額外處理,直接將其向上層返回。
if err != nil {
    return err
}
  • 在處理用戶請求的 Handler 函數(shù)中(如 ShowAccount)通過 apierr.WrapC 將錯誤包裝成一個 APIError 返回。
if err != nil {
    return apierr.WrapC(CodeNotFound, err)
}
  • 最上層代碼通過在框架層面實現(xiàn)的中間件(如實現(xiàn)一個 after hook middleware)來統(tǒng)一處理錯誤,打印完整錯誤日志、發(fā)送郵件提醒等,并將安全的錯誤信息返回給前端。如我們實現(xiàn)的 ResponseError 函數(shù)功能。

總結(jié)

本篇文章講解了如何設(shè)計一個規(guī)范的錯誤碼以及與之配套的錯誤包。

我參考了一些開源的 API 錯誤碼設(shè)計方案,并結(jié)合我自己的實際工作經(jīng)驗,給出了我認(rèn)為比較合理的錯誤碼設(shè)計方案。

同時也針對這個錯誤碼方案,設(shè)計了一個配套的錯誤包,來簡化使用過程,并給出了我的一些使用建議。

錯誤包中記錄錯誤堆棧部分的代碼參考了 pkg/errors 包實現(xiàn),感興趣的同學(xué)可以點擊進去進行進一步學(xué)習(xí)。

本文完整代碼實現(xiàn)我放在了 Github 上,供你參考使用。

以上就是go語言規(guī)范RESTful API業(yè)務(wù)錯誤處理的詳細(xì)內(nèi)容,更多關(guān)于go RESTful API錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 淺談JWT在GO中的使用方法及原理

    淺談JWT在GO中的使用方法及原理

    JWT是一種基于?JSON?的開放標(biāo)準(zhǔn),用于在網(wǎng)絡(luò)應(yīng)用間傳遞聲明,JWT被設(shè)計為可安全地將用戶身份驗證和授權(quán)數(shù)據(jù)作為?JSON?對象在各個應(yīng)用程序之間傳遞,本文將詳細(xì)給大家介紹JWT原理及在Go中的用法,需要的朋友可以參考下
    2023-05-05
  • 淺析Go 字符串指紋

    淺析Go 字符串指紋

    這篇文章主要介紹了Go 字符串指紋的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)go語言,感興趣的朋友可以了解下
    2020-09-09
  • go語言區(qū)塊鏈實戰(zhàn)實現(xiàn)簡單的區(qū)塊與區(qū)塊鏈

    go語言區(qū)塊鏈實戰(zhàn)實現(xiàn)簡單的區(qū)塊與區(qū)塊鏈

    這篇文章主要為大家介紹了go語言區(qū)塊鏈的實戰(zhàn)學(xué)習(xí),來實現(xiàn)簡單的區(qū)塊與區(qū)塊鏈?zhǔn)纠^程,有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2021-10-10
  • 關(guān)于golang?struct?中的?slice?無法原子賦值的問題

    關(guān)于golang?struct?中的?slice?無法原子賦值的問題

    這篇文章主要介紹了為什么?golang?struct?中的?slice?無法原子賦值的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2024-01-01
  • Go使用chan或context退出協(xié)程示例詳解

    Go使用chan或context退出協(xié)程示例詳解

    這篇文章主要為大家介紹了Go使用chan或context退出協(xié)程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-08-08
  • Go語言中錯誤處理實例分析

    Go語言中錯誤處理實例分析

    這篇文章主要介紹了Go語言中錯誤處理,實例分析了Go語言中針對錯誤處理的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • 詳解Go語言的內(nèi)存模型及堆的分配管理

    詳解Go語言的內(nèi)存模型及堆的分配管理

    這篇筆記主要介紹Go內(nèi)存分配和Go內(nèi)存管理,會輕微涉及內(nèi)存申請和釋放,以及Go垃圾回收,文中有詳細(xì)的代碼示例以及圖片介紹,需要的朋友可以參考下
    2023-05-05
  • go?mode?tidy出現(xiàn)報錯go:?warning:?“all“?matched?no?packages的解決方法

    go?mode?tidy出現(xiàn)報錯go:?warning:?“all“?matched?no?package

    使用go的時候我們一般都會使用go?mode管理,下面這篇文章主要給大家介紹了關(guān)于go?mode?tidy出現(xiàn)報錯go:?warning:?“all“?matched?no?packages的解決方法,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2022-08-08
  • Golang之sync.Pool使用詳解

    Golang之sync.Pool使用詳解

    這篇文章主要介紹了Golang之sync.Pool使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • 一文帶你搞懂Golang依賴注入的設(shè)計與實現(xiàn)

    一文帶你搞懂Golang依賴注入的設(shè)計與實現(xiàn)

    在現(xiàn)代的 web 框架里面,基本都有實現(xiàn)了依賴注入的功能,可以讓我們很方便地對應(yīng)用的依賴進行管理。今天我們來看看 go 里面實現(xiàn)依賴注入的一種方式,感興趣的可以了解一下
    2023-01-01

最新評論