偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go 開發(fā)者必修課:如何優(yōu)雅的設(shè)計(jì)和實(shí)現(xiàn) API 接口的錯(cuò)誤返回

開發(fā) 后端
本節(jié)課探討了錯(cuò)誤返回的優(yōu)秀實(shí)踐,比較了兩種常見的錯(cuò)誤返回方式,并選擇了更符合企業(yè)級(jí)開發(fā)需求的第二種方式。

在 Go 項(xiàng)目開發(fā)中,有很多基礎(chǔ)的 Go 包需要我們?nèi)ピO(shè)計(jì)。其中,錯(cuò)誤包是 Go 項(xiàng)目開發(fā)必須要考慮的一個(gè)設(shè)計(jì)。

錯(cuò)誤包在 Go 項(xiàng)目開發(fā)中主要用來(lái)返回錯(cuò)誤或者打印錯(cuò)誤。返回錯(cuò)誤時(shí),既需在代碼內(nèi)返回錯(cuò)誤,又需要將錯(cuò)誤返回給用戶。在設(shè)計(jì)和實(shí)現(xiàn)錯(cuò)誤包的時(shí)候,需要考慮上述使用場(chǎng)景。

一、錯(cuò)誤返回方法

在 Go 項(xiàng)目開發(fā)中,錯(cuò)誤的返回方式通常有以下兩種:

  • 始終返回 HTTP 200 狀態(tài)碼,并在 HTTP 返回體中返回錯(cuò)誤信息;
  • 返回 HTTP 400 狀態(tài)碼(Bad Request),并在 HTTP 返回體中返回錯(cuò)誤信息。

方式一:成功返回,返回體中返回錯(cuò)誤信息

例如 Facebook API 的錯(cuò)誤返回設(shè)計(jì),始終返回 200 HTTP 狀態(tài)碼:

{
  "error": {
    "message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
    "type": "OAuthException",
    "code": 2500,
    "fbtrace_id": "xxxxxxxxxxx"
  }
}

在上述錯(cuò)誤返回的實(shí)現(xiàn)方式中,HTTP 狀態(tài)碼始終固定返回 200,僅需關(guān)注業(yè)務(wù)錯(cuò)誤碼,整體實(shí)現(xiàn)較為簡(jiǎn)單。然而,此方式存在一個(gè)明顯的缺點(diǎn):對(duì)于每一次 HTTP 請(qǐng)求,既需要檢查 HTTP 狀態(tài)碼以判斷請(qǐng)求是否成功,還需要解析響應(yīng)體以獲取業(yè)務(wù)錯(cuò)誤碼,從而判斷業(yè)務(wù)邏輯是否成功。理想情況下,我們期望客戶端對(duì)成功的 HTTP 請(qǐng)求能夠直接將響應(yīng)體解析為需要的 Go 結(jié)構(gòu)體,并進(jìn)行后續(xù)的業(yè)務(wù)邏輯處理,而不用再判斷請(qǐng)求是否成功。

方式二:失敗返回,返回體中返回錯(cuò)誤信息

Twitter API 的錯(cuò)誤返回設(shè)計(jì)會(huì)根據(jù)錯(cuò)誤類型返回對(duì)應(yīng)的 HTTP 狀態(tài)碼,并在返回體中返回錯(cuò)誤信息和自定義業(yè)務(wù)錯(cuò)誤碼。成功的業(yè)務(wù)請(qǐng)求則返回 200 HTTP 狀態(tài)碼。例如:

HTTP/1.1 400 Bad Request
x-connection-hash: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
set-cookie: guest_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Date: Thu, 01 Jun 201703:04:23 GMT
Content-Length: 62
x-response-time: 5
strict-transport-security: max-age=631138519
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Server: tsa_b

{
"errors": [
    {
      "code": 215,
      "message": "Bad Authentication data."
    }
  ]
}

方式二相比方式一,對(duì)于成功的請(qǐng)求不需要再次判錯(cuò)。然而,方式二還可以進(jìn)一步優(yōu)化:整數(shù)格式的業(yè)務(wù)錯(cuò)誤碼 215 可讀性較差,用戶無(wú)法從 215 直接獲取任何有意義的信息。建議將其替換為語(yǔ)義化的字符串,例如:NotFound.PostNotFound。

Twitter API 返回的錯(cuò)誤是一個(gè)數(shù)組,在實(shí)際開發(fā)獲取錯(cuò)誤時(shí),需要先判斷數(shù)組是否為空,如不為空,再?gòu)臄?shù)組中獲取錯(cuò)誤,開發(fā)復(fù)雜度較高。建議采用更簡(jiǎn)單的錯(cuò)誤返回格式:

{
  "code": "InvalidParameter.BadAuthenticationData",
  "message": "Bad Authentication data."
}

需要特別注意的是,message 字段會(huì)直接展示給外部用戶,因此必須確保其內(nèi)容不包含敏感信息,例如數(shù)據(jù)庫(kù)的 id 字段、內(nèi)部組件的 IP 地址、用戶名等信息。返回的錯(cuò)誤信息中,還可以根據(jù)需要返回更多字段,例如:錯(cuò)誤指引文檔 URL 等。

二、miniblog 錯(cuò)誤返回設(shè)計(jì)和實(shí)現(xiàn)

miniblog 項(xiàng)目錯(cuò)誤返回格式采用了方式二,在接口失敗時(shí)返回對(duì)應(yīng)的 HTTP/gRPC 狀態(tài)碼,并在返回體中返回具體的錯(cuò)誤信息,例如:

HTTP/1.1 404 Not Found
...
{
  "code": "NotFound.UserNotFound",
  "message": "User not found."
}

在錯(cuò)誤返回方式二中,需要返回一個(gè)業(yè)務(wù)錯(cuò)誤碼。返回業(yè)務(wù)錯(cuò)誤碼可以帶來(lái)以下好處:

  • 快速定位問(wèn)題:開發(fā)人員可以借助錯(cuò)誤碼迅速定位問(wèn)題,并精確到具體的代碼行。例如,錯(cuò)誤碼可以直接指示問(wèn)題的含義,同時(shí)通過(guò)工具(如 grep)輕松定位到錯(cuò)誤碼在代碼中的具體位置;
  • 便于排查問(wèn)題:用戶能夠通過(guò)錯(cuò)誤碼判斷接口失敗的原因,并將錯(cuò)誤碼提供給開發(fā)人員,以便快速定位問(wèn)題并進(jìn)行排查;
  • 承載豐富信息:錯(cuò)誤碼通常包含了詳細(xì)的信息,例如錯(cuò)誤的級(jí)別、所屬錯(cuò)誤類別以及具體的錯(cuò)誤描述。這些錯(cuò)誤信息可以幫助用戶和開發(fā)者快速定位問(wèn)題;
  • 靈活定義:錯(cuò)誤碼由開發(fā)者根據(jù)需要靈活定義,不依賴和受限于第三方框架,例如 net/http 和 google.golang.org/grpc;
  • 便于邏輯判斷:在業(yè)務(wù)開發(fā)中,判斷錯(cuò)誤類別以執(zhí)行對(duì)應(yīng)的邏輯處理是一個(gè)常見需求。通過(guò)自定義錯(cuò)誤碼,可以輕松實(shí)現(xiàn)。例如:
import "errors"
import "path/to/errno"

if errors.Is(err, errno.InternalServerError) {
    // 對(duì)應(yīng)錯(cuò)誤處理邏輯
}

1. 制定錯(cuò)誤碼規(guī)范

錯(cuò)誤碼是直接暴露給用戶的,因此需要設(shè)計(jì)一個(gè)易讀、易懂且規(guī)范化的錯(cuò)誤碼。在設(shè)計(jì)錯(cuò)誤碼時(shí)可以根據(jù)實(shí)際需求自行設(shè)計(jì),也可以參考其他優(yōu)秀的設(shè)計(jì)方案。

一般來(lái)說(shuō),當(dāng)調(diào)研某項(xiàng)技術(shù)實(shí)現(xiàn)時(shí),建議優(yōu)先參考各大公有云廠商的實(shí)現(xiàn)方式,例如騰訊云、阿里云、華為云等。這些公有云廠商直接面向企業(yè)和個(gè)人,專注于技術(shù)本身,擁有強(qiáng)大的技術(shù)團(tuán)隊(duì),因此它們的設(shè)計(jì)與實(shí)現(xiàn)具有很高的參考價(jià)值。

經(jīng)過(guò)調(diào)研,此處采用了騰訊云 API 3.0 的錯(cuò)誤碼設(shè)計(jì)規(guī)范,并將規(guī)范文檔保存在項(xiàng)目的文檔目錄中:docs/devel/zh-CN/conversions/error_code.md[2]。

騰訊云采用了兩級(jí)錯(cuò)誤碼設(shè)計(jì)。以下是兩級(jí)錯(cuò)誤碼設(shè)計(jì)相較于簡(jiǎn)單錯(cuò)誤碼(如 215、InvalidParameter)的優(yōu)勢(shì):

  • 語(yǔ)義化: 語(yǔ)義化的錯(cuò)誤碼可以通過(guò)名字直接反映錯(cuò)誤的類型,便于快速理解錯(cuò)誤;
  • 更加靈活: 二級(jí)錯(cuò)誤碼的格式為<平臺(tái)級(jí).資源級(jí)>。其中,平臺(tái)級(jí)錯(cuò)誤碼是固定值,用于指代某一類錯(cuò)誤,客戶端可以利用該錯(cuò)誤碼進(jìn)行通用錯(cuò)誤處理。資源級(jí)錯(cuò)誤碼則用于更精確的錯(cuò)誤定位。此外,服務(wù)端既可根據(jù)需求自定義錯(cuò)誤碼,也可使用默認(rèn)錯(cuò)誤碼。

miniblog 項(xiàng)目預(yù)定義了一些平臺(tái)級(jí)錯(cuò)誤碼,如下表所示。

錯(cuò)誤碼

錯(cuò)誤描述

錯(cuò)誤類型

OK

請(qǐng)求成功

-

InternalError

內(nèi)部錯(cuò)誤

1

NotFound

資源不存在

0

BindError

綁定失敗,解析請(qǐng)求體失敗

0

InvalidArgument

參數(shù)錯(cuò)誤(包括參數(shù)類型、格式、值等錯(cuò)誤)

0

Unauthenticated

認(rèn)證失敗

0

PermissionDenied

授權(quán)失敗

0

OperationFailed

操作失敗

2

上表中,錯(cuò)誤類型 0 代表客戶端錯(cuò)誤,1 代表服務(wù)端錯(cuò)誤,2 代表客戶端錯(cuò)誤/服務(wù)端錯(cuò)誤,- 代表請(qǐng)求成功。

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

開發(fā)一個(gè)錯(cuò)誤包,需要先為錯(cuò)誤包起一個(gè)易讀、易理解的包名。在 Go 項(xiàng)目開發(fā)中,如果自定義包的名稱如 errors、context 等,會(huì)與 Go 標(biāo)準(zhǔn)庫(kù)中已存在的 errors 或 context 包發(fā)生命名沖突,如果代碼中需要同時(shí)使用自定義包與標(biāo)準(zhǔn)庫(kù)包時(shí),通常會(huì)通過(guò)為標(biāo)準(zhǔn)庫(kù)包起別名的方式解決。例如,可以通過(guò) import stderrors "errors" 來(lái)為標(biāo)準(zhǔn)庫(kù)的 errors 包定義別名。

為了避免頻繁使用這種起別名的操作,在開發(fā)自定義包時(shí),可以從包命名上避免與標(biāo)準(zhǔn)庫(kù)包名沖突。建議將可能沖突的包命名為 <沖突包原始名>x,其名稱中的“x”代表擴(kuò)展(extended)或?qū)嶒?yàn)(experimental)。這種命名方式是一種擴(kuò)展命名約定,通常用于表示此包是對(duì)標(biāo)準(zhǔn)庫(kù)中已有包功能的擴(kuò)展或補(bǔ)充。需要注意的是,這并非 Go 語(yǔ)言的官方規(guī)范,而是開發(fā)者為了防止命名沖突、增強(qiáng)語(yǔ)義所采用的命名方式。miniblog 項(xiàng)目的自定義 contextx 包也采用了這種命名風(fēng)格。

因此,為了避免與標(biāo)準(zhǔn)庫(kù)的 errors 包命名沖突,miniblog 項(xiàng)目的錯(cuò)誤包命名為 errorsx,寓意為“擴(kuò)展的錯(cuò)誤處理包”。

由于 miniblog 項(xiàng)目的錯(cuò)誤包命名為 errorsx,為保持命名一致性,定義了一個(gè)名為 ErrorX 的結(jié)構(gòu)體,用于描述錯(cuò)誤信息,具體定義如下:

// ErrorX 定義了 OneX 項(xiàng)目體系中使用的錯(cuò)誤類型,用于描述錯(cuò)誤的詳細(xì)信息.
type ErrorX struct {
    // Code 表示錯(cuò)誤的 HTTP 狀態(tài)碼,用于與客戶端進(jìn)行交互時(shí)標(biāo)識(shí)錯(cuò)誤的類型.
    Code int`json:"code,omitempty"`

    // Reason 表示錯(cuò)誤發(fā)生的原因,通常為業(yè)務(wù)錯(cuò)誤碼,用于精準(zhǔn)定位問(wèn)題.
    Reason string`json:"reason,omitempty"`

    // Message 表示簡(jiǎn)短的錯(cuò)誤信息,通常可直接暴露給用戶查看.
    Message string`json:"message,omitempty"`

    // Metadata 用于存儲(chǔ)與該錯(cuò)誤相關(guān)的額外元信息,可以包含上下文或調(diào)試信息.
    Metadata map[string]string`json:"metadata,omitempty"`
}

ErrorX 是一個(gè)錯(cuò)誤類型,因此需要實(shí)現(xiàn) Error 方法:

// Error 實(shí)現(xiàn) error 接口中的 `Error` 方法.
func (err *ErrorX) Error() string {
    return fmt.Sprintf("error: code = %d reason = %s message = %s metadata = %v", err.Code, err.Reason, err.Message, err.Metadata)
}

Error() 返回的錯(cuò)誤信息中,包含了 HTTP 狀態(tài)碼、錯(cuò)誤發(fā)生的原因、錯(cuò)誤信息和額外的錯(cuò)誤元信息。通過(guò)這些詳盡的錯(cuò)誤信息返回,幫助開發(fā)者快速定位錯(cuò)誤。

提示

miniblog 項(xiàng)目屬于 OneX 技術(shù)體系中的一個(gè)實(shí)戰(zhàn)項(xiàng)目,其設(shè)計(jì)和實(shí)現(xiàn)方式跟 OneX 技術(shù)體系中的其他項(xiàng)目保持一致??紤]到包的復(fù)用性,errorsx 包的實(shí)現(xiàn)位于 onexstack[3] 項(xiàng)目根目錄下的 pkg/errorsx 目錄中。

在 Go 項(xiàng)目開發(fā)中,發(fā)生錯(cuò)誤的原因有很多,大多數(shù)情況下,開發(fā)者希望將真實(shí)的錯(cuò)誤信息返回給用戶。因此,還需要提供一個(gè)方法用來(lái)設(shè)置 ErrorX 結(jié)構(gòu)體中的 Message 字段。同樣的,還需要提供設(shè)置 Metadata 字段的方法。為了滿足上述訴求,給 ErrorX 增加 WithMessage、WithMetadata、KV 三個(gè)方法。實(shí)現(xiàn)方式如下述代碼所示。

// WithMessage 設(shè)置錯(cuò)誤的 Message 字段.
func (err *ErrorX) WithMessage(format string, args ...any) *ErrorX {
    err.Message = fmt.Sprintf(format, args...)
    return err
}

// WithMetadata 設(shè)置元數(shù)據(jù).
func (err *ErrorX) WithMetadata(md map[string]string) *ErrorX {
    err.Metadata = md
    return err
}

// KV 使用 key-value 對(duì)設(shè)置元數(shù)據(jù).
func (err *ErrorX) KV(kvs ...string) *ErrorX {
    if err.Metadata == nil {
        err.Metadata = make(map[string]string) // 初始化元數(shù)據(jù)映射
    }

    for i := 0; i < len(kvs); i += 2 {
        // kvs 必須是成對(duì)的
        if i+1 < len(kvs) {
            err.Metadata[kvs[i]] = kvs[i+1]
        }
    }
    return err
}

在上述代碼中,設(shè)置 Message、Metadata 字段的方法名分別為 WithMessage、WithMetadata。WithXXX,在 Go 項(xiàng)目開發(fā)中是一種很常見的命名方式,寓意是:設(shè)置 XXX。KV 方法則以追加的方式給 Metadata 增加鍵值對(duì)。WithMessage、WithMetadata、KV 都返回了 *ErrorX 類型的實(shí)例,目的是為了實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,例如:

err := new(ErrorX)
err.WithMessage("Message").WithMetadata(map[string]string{"key":"value"})

在 Go 項(xiàng)目開發(fā)中,鏈?zhǔn)秸{(diào)用(chained method calls)是一種常見的設(shè)計(jì)模式,該模式通過(guò)在方法中返回對(duì)象自身,使多個(gè)方法調(diào)用可以連續(xù)進(jìn)行。鏈?zhǔn)秸{(diào)用的好處在于:簡(jiǎn)化代碼、提高可讀性、減少錯(cuò)誤可能性和增強(qiáng)擴(kuò)展性,尤其是在對(duì)象構(gòu)造或逐步修改操作時(shí),非常高效直觀。合理使用鏈?zhǔn)秸{(diào)用可以顯著提升代碼的質(zhì)量和開發(fā)效率,同時(shí)讓接口設(shè)計(jì)更加優(yōu)雅。

errorsx 包的設(shè)計(jì)目標(biāo)不僅適用于 HTTP 接口的錯(cuò)誤返回,還適用于 gRPC 接口的錯(cuò)誤返回。因此,ErrorX 結(jié)構(gòu)體還實(shí)現(xiàn)了 GRPCStatus() 方法。GRPCStatus() 方法的作用是將自定義錯(cuò)誤類型 ErrorX 轉(zhuǎn)換為 gRPC 的 status.Status 類型,用于生成 gRPC 標(biāo)準(zhǔn)化的錯(cuò)誤返回信息(包括錯(cuò)誤碼、錯(cuò)誤消息及詳細(xì)錯(cuò)誤信息),從而滿足 gRPC 框架的錯(cuò)誤處理要求。GRPCStatus() 方法實(shí)現(xiàn)如下:

// GRPCStatus 返回 gRPC 狀態(tài)表示.
func (err *ErrorX) GRPCStatus() *status.Status {
    details := errdetails.ErrorInfo{Reason: err.Reason, Metadata: err.Metadata}
    s, _ := status.New(httpstatus.ToGRPCCode(err.Code), err.Message).WithDetails(&details)
    return s
}

在 Go 項(xiàng)目開發(fā)中,通常需要將一個(gè) error 類型的錯(cuò)誤 err,解析為 *ErrorX 類型,并獲取 *ErrorX 中的 Code 字段和 Reason 字段的值。Code 字段可用來(lái)設(shè)置 HTTP 狀態(tài)碼,Reason 字段可用來(lái)判斷錯(cuò)誤類型。為此,errorsx 包實(shí)現(xiàn)了 FromError、Code、Reason 方法,具體實(shí)現(xiàn)如下:

// Code 返回錯(cuò)誤的 HTTP 代碼.
func Code(err error) int {
    if err == nil {
        return http.StatusOK //nolint:mnd
    }
    return FromError(err).Code
}

// Reason 返回特定錯(cuò)誤的原因.
func Reason(err error) string {
    if err == nil {
        return ErrInternal.Reason
    }
    return FromError(err).Reason
}

// FromError 嘗試將一個(gè)通用的 error 轉(zhuǎn)換為自定義的 *ErrorX 類型.
func FromError(err error) *ErrorX {
    // 如果傳入的錯(cuò)誤是 nil,則直接返回 nil,表示沒有錯(cuò)誤需要處理.
    if err == nil {
        returnnil
    }

    // 檢查傳入的 error 是否已經(jīng)是 ErrorX 類型的實(shí)例.
    // 如果錯(cuò)誤可以通過(guò) errors.As 轉(zhuǎn)換為 *ErrorX 類型,則直接返回該實(shí)例.
    if errx := new(ErrorX); errors.As(err, &errx) {
        return errx
    }

    // gRPC 的 status.FromError 方法嘗試將 error 轉(zhuǎn)換為 gRPC 錯(cuò)誤的 status 對(duì)象.
    // 如果 err 不能轉(zhuǎn)換為 gRPC 錯(cuò)誤(即不是 gRPC 的 status 錯(cuò)誤),
    // 則返回一個(gè)帶有默認(rèn)值的 ErrorX,表示是一個(gè)未知類型的錯(cuò)誤.
    gs, ok := status.FromError(err)
    if !ok {
        return New(ErrInternal.Code, ErrInternal.Reason, err.Error())
    }

    // 如果 err 是 gRPC 的錯(cuò)誤類型,會(huì)成功返回一個(gè) gRPC status 對(duì)象(gs).
    // 使用 gRPC 狀態(tài)中的錯(cuò)誤代碼和消息創(chuàng)建一個(gè) ErrorX.
    ret := New(httpstatus.FromGRPCCode(gs.Code()), ErrInternal.Reason, gs.Message())

    // 遍歷 gRPC 錯(cuò)誤詳情中的所有附加信息(Details).
    for _, detail := range gs.Details() {
        if typed, ok := detail.(*errdetails.ErrorInfo); ok {
            ret.Reason = typed.Reason
            return ret.WithMetadata(typed.Metadata)
        }
    }

    return ret
}

在 Go 項(xiàng)目開發(fā)中,經(jīng)常還要對(duì)比一個(gè) error 類型的錯(cuò)誤 err 是否是某個(gè)預(yù)定義錯(cuò)誤,因此 *ErrorX 也需要實(shí)現(xiàn)一個(gè) Is 方法,Is 方法實(shí)現(xiàn)如下:

// Is 判斷當(dāng)前錯(cuò)誤是否與目標(biāo)錯(cuò)誤匹配.
// 它會(huì)遞歸遍歷錯(cuò)誤鏈,并比較 ErrorX 實(shí)例的 Code 和 Reason 字段.
// 如果 Code 和 Reason 均相等,則返回 true;否則返回 false.
func (err *ErrorX) Is(target error) bool {
    if errx := new(ErrorX); errors.As(target, &errx) {
        return errx.Code == err.Code && errx.Reason == err.Reason
    }
    return false
}

Is 方法中,通過(guò)對(duì)比 Code 和 Reason 字段,來(lái)判斷 target 錯(cuò)誤是否是指定的預(yù)定義錯(cuò)誤。注意,Is 方法中,沒有對(duì)比 Message 字段的值,這是因?yàn)?nbsp;Message 字段的值通常是動(dòng)態(tài)的,而錯(cuò)誤類型的定義不依賴于 Message。

至此,成功為 miniblog 開發(fā)了一個(gè)滿足項(xiàng)目需求的錯(cuò)誤包 errorsx,代碼完整實(shí)現(xiàn)見 onexstack 項(xiàng)目的 pkg/errorsx/errorsx.go[4] 文件。

3. miniblog 錯(cuò)誤碼定義

在實(shí)現(xiàn)了 errorsx 錯(cuò)誤包之后,便可以根據(jù)需要預(yù)定義項(xiàng)目需要的錯(cuò)誤。這些錯(cuò)誤,可以在代碼中便捷的引用。通過(guò)直接引用預(yù)定義錯(cuò)誤,不僅可以提高開發(fā)效率,還可以保持整個(gè)項(xiàng)目的錯(cuò)誤返回是一致的。

miniblog 的預(yù)定義錯(cuò)誤定義在 internal/pkg/errno[5] 目錄下。一些基礎(chǔ)錯(cuò)誤定義如下:

var (
    // OK 代表請(qǐng)求成功.
    OK = &errorsx.ErrorX{Code: http.StatusOK, Message: ""}

    // ErrInternal 表示所有未知的服務(wù)器端錯(cuò)誤.
    ErrInternal = errorsx.ErrInternal

    ...

    // ErrPageNotFound 表示頁(yè)面未找到.
    ErrPageNotFound = &errorsx.ErrorX{Code: http.StatusNotFound, Reason: "NotFound.PageNotFound", Message: "Page not found."}
    ...
)

更完整的預(yù)定義錯(cuò)誤,可直接查看 internal/pkg/errno 中的錯(cuò)誤定義文件。預(yù)定義錯(cuò)誤保存在 internal/pkg 目錄中,是因?yàn)檫@些錯(cuò)誤跟 miniblog 項(xiàng)目耦合,不是通用的錯(cuò)誤定義。

至此,miniblog 成功實(shí)現(xiàn)了錯(cuò)誤返回代碼的實(shí)現(xiàn),完整代碼見分支 feature/s08[6]。

4. miniblog 錯(cuò)誤返回規(guī)范

為了標(biāo)準(zhǔn)化接口錯(cuò)誤返回,提高接口錯(cuò)誤返回的易讀性,miniblog 制定了以下錯(cuò)誤返回規(guī)范:

  • 所有接口都要返回 errorsx.ErrorX 類型的錯(cuò)誤;
  • 建議在錯(cuò)誤的原始位置,使用 errno.ErrXXX 方式返回 miniblog 自定義錯(cuò)誤類型,其他位置直接透?jìng)髯远x錯(cuò)誤:
package main

import (
    "github.com/onexstack/miniblog/internal/pkg/errno"
    "github.com/onexstack/miniblog/internal/pkg/log"
)

func main() {
    if err := validateUser(); err != nil {
        panic(err)
    }
}

func validatePassword(password string) error {
    iflen(password) < 6 {
        log.Errorw("Password is too short")
        // 在錯(cuò)誤最原始位置封裝自定義錯(cuò)誤
        // 方式1:不帶自定義信息的錯(cuò)誤返回
        return errno.ErrPasswordInvalid
        // 方式2:帶有自定義信息的錯(cuò)誤返回
        //return errno.ErrPasswordInvalid.WithMessage("Password is too short")
    }
    returnnil
}

func validateUser() error {
    // 直接透?jìng)?validatePassword 返回的自定義錯(cuò)誤
    if err := validatePassword("test"); err != nil {
        return err
    }
    returnnil
}

三、minilbog 錯(cuò)誤包測(cè)試

本節(jié)就來(lái)測(cè)試下 errorsx 錯(cuò)誤包及 errno 錯(cuò)誤碼。測(cè)試代碼保存在 examples/errorsx/main.go[7] 文件中,代碼如下:

package main

import (
    "fmt"

    "github.com/onexstack/onexstack/pkg/errorsx"

    "github.com/onexstack/miniblog/internal/pkg/errno"
)

func main() {
    // 創(chuàng)建了一個(gè) ErrorX 錯(cuò)誤,表示數(shù)據(jù)庫(kù)連接失敗。
    // Code: 500,表明是服務(wù)器內(nèi)部錯(cuò)誤。
    // Reason: "InternalError.DBConnection",表示錯(cuò)誤的具體分類。
    // Message: "Something went wrong: DB connection failed",表示該錯(cuò)誤的具體信息。
    errx := errorsx.New(500, "InternalError.DBConnection", "Something went wrong: %s", "DB connection failed")

    // fmt.Println 會(huì)調(diào)用 errx 的 Error 方法,輸出:
    // error: code = 500 reason = InternalError.DBConnection message = Something went wrong: DB connection failed metadata = map[]
    fmt.Println(errx)

    // 給錯(cuò)誤添加元數(shù)據(jù),增強(qiáng)錯(cuò)誤的上下文信息,便于調(diào)試和追蹤。
    errx.WithMetadata(map[string]string{
        "user_id":    "12345",   // 添加用戶 ID 信息
        "request_id": "abc-def", // 添加請(qǐng)求 ID 信息
    })

    // 繼續(xù)向錯(cuò)誤中添加元數(shù)據(jù),這次使用了 KV 方法,它是一種更加簡(jiǎn)潔的方式,用 key-value 的模式逐一設(shè)置元數(shù)據(jù)。
    // 這里添加 trace_id 信息,用于關(guān)聯(lián)分布式鏈路信息。
    errx.KV("trace_id", "xyz-789")

    // 使用 WithMessage 方法更新錯(cuò)誤的 Message 字段。
    // 更新后的 Message 是:Updated message: retry failed。
    // Note: 更新消息字段并不會(huì)影響 Code、Reason 和 Metadata,它只是說(shuō)明錯(cuò)誤的上下文發(fā)生了變化。
    errx.WithMessage("Updated message: %s", "retry failed")

    // 再次打印 errx,此時(shí)的內(nèi)容已經(jīng)發(fā)生了變化:
    // error: code = 500 reason = InternalError.DBConnection message = Updated message: retry failed metadata = map[request_id:abc-def trace_id:xyz-789 user_id:12345]
    // 元數(shù)據(jù)也會(huì)被一并輸出。
    fmt.Println(errx)

    // 調(diào)用 doSomething 函數(shù),生成一個(gè)錯(cuò)誤,并打印它,這里返回一個(gè)更新過(guò) Message 字段的預(yù)定義錯(cuò)誤 errno.ErrUsernameInvalid。
    someerr := doSomething()
    // 打印錯(cuò)誤。
    // error: code = 400 reason = InvalidArgument.UsernameInvalid message = Username is too short metadata = map[]
    fmt.Println(someerr)

    // 調(diào)用預(yù)定義錯(cuò)誤 errno.ErrUsernameInvalid 的 Is 方法,判斷 someerr 是否屬于該類型錯(cuò)誤。
    // Is 方法會(huì)比較 Code 和 Reason 字段(不會(huì)比較 Message 字段),如果兩者一致,則返回 true。
    // 因?yàn)?doSomething 返回的錯(cuò)誤正是 errno.ErrUsernameInvalid 的實(shí)例,因此這里輸出 true。
    fmt.Println(errno.ErrUsernameInvalid.Is(someerr))

    // 調(diào)用另外一個(gè)預(yù)定義錯(cuò)誤 errno.ErrPasswordInvalid 的 Is 方法,比較 someerr 是否屬于該錯(cuò)誤。
    // 因?yàn)?Reason 和 Code 不匹配(someerr 是 username 錯(cuò)誤,而不是 password 錯(cuò)誤),因此返回 false。
    fmt.Println(errno.ErrPasswordInvalid.Is(someerr))
}

// 定義一個(gè)函數(shù) doSomething,返回一個(gè)錯(cuò)誤
func doSomething() error {
    // 這里返回了一個(gè)已經(jīng)定義的錯(cuò)誤類型 errno.ErrUsernameInvalid,但動(dòng)態(tài)地設(shè)置了 Message 字段為 "Username is too short"。
    // 重點(diǎn)是:雖然錯(cuò)誤的 Message 不同,但錯(cuò)誤的 Code 和 Reason 是一致的,這方便使用 Is 方法進(jìn)行類型判斷而不受具體內(nèi)容影響。
    return errno.ErrUsernameInvalid.WithMessage("Username is too short")
}

上述代碼已有詳盡的代碼注釋,這里不再詳細(xì)介紹。

四、總結(jié)

本節(jié)課探討了錯(cuò)誤返回的優(yōu)秀實(shí)踐,比較了兩種常見的錯(cuò)誤返回方式,并選擇了更符合企業(yè)級(jí)開發(fā)需求的第二種方式。

通過(guò)定義 ErrorX 結(jié)構(gòu)體,miniblog 項(xiàng)目實(shí)現(xiàn)了包含 HTTP/gRPC 狀態(tài)碼、業(yè)務(wù)錯(cuò)誤碼、錯(cuò)誤信息及元數(shù)據(jù)的錯(cuò)誤返回機(jī)制。此外,還為 ErrorX 提供了便捷的字段設(shè)置方法,方便開發(fā)者快速構(gòu)造和返回錯(cuò)誤。

責(zé)任編輯:趙寧寧 來(lái)源: 令飛編程
相關(guān)推薦

2009-09-29 10:35:42

Linux系統(tǒng)系統(tǒng)提速Linux

2015-07-29 10:25:05

數(shù)據(jù)開發(fā)產(chǎn)品必修課

2010-11-25 10:55:34

2025-05-22 08:25:00

C++開發(fā)資源管理

2025-02-05 11:00:00

開發(fā)Java對(duì)象模型

2014-02-17 09:22:37

2022-03-11 10:53:32

UML建模語(yǔ)言

2022-09-19 10:04:44

人工智能AIIT領(lǐng)導(dǎo)者

2022-08-15 15:03:57

數(shù)字化轉(zhuǎn)型數(shù)字技術(shù)中小企業(yè)

2009-02-10 15:08:41

2020-10-23 10:02:40

GRASPRDD模式

2018-08-06 11:07:03

技術(shù)管理者識(shí)人

2012-01-06 14:10:42

數(shù)據(jù)質(zhì)量管理大數(shù)據(jù)數(shù)據(jù)管理

2023-09-27 22:18:41

2020-01-13 16:26:57

AI人工智能機(jī)器

2023-05-15 09:51:23

算力開發(fā)

2018-04-28 10:05:17

2013-02-28 09:46:18

程序員巖機(jī)Hacker News

2025-02-12 08:04:54

代碼Ordersetter

2014-06-23 15:37:50

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)