溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

go語(yǔ)言規(guī)范RESTful?API業(yè)務(wù)錯(cuò)誤處理的方法是什么

發(fā)布時(shí)間:2023-03-08 10:55:46 來(lái)源:億速云 閱讀:123 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇“go語(yǔ)言規(guī)范RESTful API業(yè)務(wù)錯(cuò)誤處理的方法是什么”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“go語(yǔ)言規(guī)范RESTful API業(yè)務(wù)錯(cuò)誤處理的方法是什么”文章吧。

    錯(cuò)誤碼

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

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

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

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

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

    錯(cuò)誤碼功能

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

    • 錯(cuò)誤碼必須是唯一的。只有錯(cuò)誤碼是唯一的才方便在程序出錯(cuò)時(shí)快速定位問(wèn)題,不然程序出錯(cuò),返回錯(cuò)誤碼不唯一,想要根據(jù)錯(cuò)誤碼排查問(wèn)題,就要針對(duì)這一錯(cuò)誤碼所表示的錯(cuò)誤列表進(jìn)行逐一排查。

    • 錯(cuò)誤碼需要是可閱讀的。意思是說(shuō),通過(guò)錯(cuò)誤碼,我們就能快速定位到是系統(tǒng)的哪個(gè)組件出現(xiàn)了錯(cuò)誤,并且知道錯(cuò)誤的類(lèi)型,不然也談不上叫「業(yè)務(wù)錯(cuò)誤碼」了。一個(gè)清晰可讀的錯(cuò)誤碼在微服務(wù)系統(tǒng)中定位問(wèn)題尤其有效。

    • 通過(guò)錯(cuò)誤碼能夠方便知道 HTTP 狀態(tài)碼。這一點(diǎn)往往容易被人忽略,不過(guò)我比較推薦這種做法,因?yàn)樵?Review 代碼時(shí),通過(guò)返回錯(cuò)誤碼,就能很容易知道接口返回 HTTP 狀態(tài)碼,這不僅方便理解代碼,更方便錯(cuò)誤的統(tǒng)一處理。

    錯(cuò)誤碼設(shè)計(jì)

    錯(cuò)誤碼調(diào)研

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

    如以下是一個(gè)阿里云 ECS 接口錯(cuò)誤的返回:

    {
    	"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 都為字符串類(lèi)型,并且還有 RequestId(當(dāng)前請(qǐng)求唯一標(biāo)識(shí))、HostId(Host 唯一標(biāo)識(shí))、Recommend(錯(cuò)誤診斷地址),可以說(shuō)這個(gè)錯(cuò)誤信息非常全面了。

    再來(lái)看下新浪網(wǎng)開(kāi)放 API 錯(cuò)誤返回結(jié)果的設(shè)計(jì):

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

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

    錯(cuò)誤代碼 20502 說(shuō)明如下:

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

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

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

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

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

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

    錯(cuò)誤碼說(shuō)明:

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

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

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

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

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

    錯(cuò)誤格式

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

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

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

    錯(cuò)誤碼實(shí)現(xiàn)

    因?yàn)槊恳粋€(gè)錯(cuò)誤碼和錯(cuò)誤信息以及錯(cuò)誤文檔地址都是一一對(duì)應(yīng)的,所以我們需要一個(gè)對(duì)象來(lái)保存這些信息,在 Go 中可以使用結(jié)構(gòu)體。

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

    type apiCode struct {
    	code int
    	msg  string
    	ref  string
    }

    這是一個(gè)私有結(jié)構(gòu)體,外部項(xiàng)目要想使用,則需要一個(gè)構(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è)計(jì)為可變參數(shù),如果不傳則默認(rèn)為空。

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

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

    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 則實(shí)現(xiàn)了 APICoder 接口。

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

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

    錯(cuò)誤包

    設(shè)計(jì)好了錯(cuò)誤碼,并不能直接使用,我們還需要一個(gè)與之配套的錯(cuò)誤包來(lái)簡(jiǎn)化錯(cuò)誤碼的使用。

    錯(cuò)誤包功能

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

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

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

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

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

    錯(cuò)誤包設(shè)計(jì)

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

    type apiError struct {
    	coder APICoder
    	cause error
    	*stack
    }

    其中 coder 用來(lái)保存實(shí)現(xiàn)了 APICoder 接口的對(duì)象,cause 用來(lái)記錄錯(cuò)誤原因,stack 用來(lái)展示錯(cuò)誤堆棧。

    錯(cuò)誤對(duì)象的構(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 通過(guò) APICoder 來(lái)創(chuàng)建錯(cuò)誤對(duì)象,第二個(gè)參數(shù)為一個(gè)可選的錯(cuò)誤原因。

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

    一個(gè)錯(cuò)誤對(duì)象必須要實(shí)現(xiàn) Error 方法:

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

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

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

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

    為了能在打印或?qū)懭肴罩緯r(shí)展示不同信息,則要實(shí)現(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) 格式化輸出時(shí)打印定制化的信息。

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

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

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

    用來(lái)進(jìn)行 JSON 序列化和反序列化的 MarshalJSON/UnmarshalJSON 方法實(shí)現(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"`
    }

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

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

    錯(cuò)誤碼及錯(cuò)誤包的使用

    使用示例

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

    使用 Gin 構(gòu)建一個(gè)簡(jiǎn)單的 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, "請(qǐng)求不合法")
    	CodeNotFound     = NewAPICode(40401001, "資源未找到")
    	CodeUnknownError = NewAPICode(50001001, "系統(tǒng)錯(cuò)誤", "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
    		}
    	}
    	// 模擬返回?cái)?shù)據(jù)庫(kù)錯(cuò)誤
    	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)
    	}
    }

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

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

    其中 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 打印錯(cuò)誤日志,輸出堆棧
    func log(err error) {
    	fmt.Println("========== log start ==========")
    	fmt.Printf("%+v\n", err)
    	fmt.Println("========== log end ==========")
    }
    // fakeSendErrorEmail 模擬將錯(cuò)誤信息發(fā)送到郵件,JSON 格式
    func fakeSendErrorEmail(err error) {
    	fmt.Println("========== error start ==========")
    	fmt.Printf("%#v\n", err)
    	fmt.Println("========== error end ==========")
    }

    ResponseOK 其實(shí)就是 Gin 框架的正常返回,ResponseError 則專(zhuān)門(mén)用來(lái)處理并返回 API 錯(cuò)誤。

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

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

    其中 apierr.ParseCoder(err) 能夠從一個(gè)錯(cuò)誤對(duì)象中獲取到實(shí)現(xiàn)了 APICoder 的錯(cuò)誤碼對(duì)象,實(shí)現(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)
    	}
    }

    這樣,我們就能夠通過(guò)一個(gè)簡(jiǎn)單的 Web Server 示例程序來(lái)演示如何使用錯(cuò)誤碼和錯(cuò)誤包了。

    可以通過(guò) go run main.go 啟動(dòng)這個(gè) Web Server。

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

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

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

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

    go語(yǔ)言規(guī)范RESTful?API業(yè)務(wù)錯(cuò)誤處理的方法是什么

    再來(lái)測(cè)試下請(qǐng)求一個(gè)不存在的賬號(hào):curl http://localhost:8080/accounts/12。

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

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

    返回結(jié)果中沒(méi)有 reference 字段,是因?yàn)閷?duì)于 reference 為空的情況,在 JSON 序列化過(guò)程中會(huì)被隱藏。

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

    ========== 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),錯(cuò)誤日志中不僅打印了錯(cuò)誤碼([40401001])和錯(cuò)誤信息(資源未找到),還打印了錯(cuò)誤原因(account not found)以及下面的錯(cuò)誤堆棧。

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

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

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

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

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

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

    ========== log start ==========
    [50001001] - 系統(tǒng)錯(cuò)誤 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)錯(cuò)誤","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ù)打印的日志,正是一個(gè) JSON 格式的結(jié)構(gòu)化日志。

    以上便是我們?cè)O(shè)計(jì)的錯(cuò)誤碼及錯(cuò)誤包在實(shí)際開(kāi)發(fā)場(chǎng)景中的應(yīng)用。

    使用建議

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

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

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

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

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

    • 400: 請(qǐng)求不合法

    • 401: 認(rèn)證失敗

    • 403: 授權(quán)失敗

    • 404: 資源未找到

    • 500: 系統(tǒng)錯(cuò)誤

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

    使用中間件來(lái)記錄錯(cuò)誤日志

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

    • 最底層代碼遇到錯(cuò)誤時(shí)通過(guò) errors.New/fmt.Errorf 來(lái)創(chuàng)建一個(gè)錯(cuò)誤對(duì)象,然后將錯(cuò)誤返回(可選擇性的記錄一條日志)。

    func Query(id int) (obj, error) {
        // do something
        return nil, fmt.Errorf("%d not found", id)
    }
    • 中間過(guò)程中處理函數(shù)遇到下層函數(shù)返回的錯(cuò)誤,不做任何額外處理,直接將其向上層返回。

    if err != nil {
        return err
    }
    • 在處理用戶(hù)請(qǐng)求的 Handler 函數(shù)中(如 ShowAccount)通過(guò) apierr.WrapC 將錯(cuò)誤包裝成一個(gè) APIError 返回。

    if err != nil {
        return apierr.WrapC(CodeNotFound, err)
    }
    • 最上層代碼通過(guò)在框架層面實(shí)現(xiàn)的中間件(如實(shí)現(xiàn)一個(gè) after hook middleware)來(lái)統(tǒng)一處理錯(cuò)誤,打印完整錯(cuò)誤日志、發(fā)送郵件提醒等,并將安全的錯(cuò)誤信息返回給前端。如我們實(shí)現(xiàn)的 ResponseError 函數(shù)功能。

    以上就是關(guān)于“go語(yǔ)言規(guī)范RESTful API業(yè)務(wù)錯(cuò)誤處理的方法是什么”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI