13 | 實現統一的錯誤返回

提示:

  • 所有體系課見專欄:Go 項目開發極速入門實戰課;
  • 歡迎加入 云原生 AI 實戰 星球,12+ 高質量體系課、20+ 高質量實戰項目助你在 AI 時代建立技術競爭力(聚焦于 Go、云原生、AI Infra);
  • 本節課最終源碼位于 fastgo 項目的 feature/s10 分支;
  • 更詳細的課程版本見:Go 項目開發中級實戰課:16 | 基礎 Go 包開發:錯誤返回設計和實現

在 Go 項目開發中,為了方便客戶端處理返回,排查錯誤,還需要實現統一的錯誤返回。統一的錯誤返回包括以下 2 個方面:

  • 錯誤格式統一:返回統一的錯誤格式,方便客戶端解析,并獲取錯誤;
  • 自定義業務錯誤碼:HTTP 的錯誤碼有限,并且不適合業務錯誤碼。所以,在實際開發中,還需要自定義業務錯誤碼。

為了實現統一的錯誤返回,接下來還需要實現錯誤包和自定義錯誤碼。

錯誤返回方法

先來看下錯誤返回的方式。在 Go 項目開發中,錯誤的返回方式通常有以下兩種:

  1. 始終返回 HTTP 200 狀態碼,并在 HTTP 返回體中返回錯誤信息;
  2. 返回 HTTP 400 狀態碼(Bad Request),并在 HTTP 返回體中返回錯誤信息。

方式一:成功返回,返回體中返回錯誤信息

例如 Facebook API 的錯誤返回設計,始終返回 200 HTTP 狀態碼:

{"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"}
}

在上述錯誤返回的實現方式中,HTTP 狀態碼始終固定返回 200,僅需關注業務錯誤碼,整體實現較為簡單。然而,此方式存在一個明顯的缺點:對于每一次 HTTP 請求,既需要檢查 HTTP 狀態碼以判斷請求是否成功,還需要解析響應體以獲取業務錯誤碼,從而判斷業務邏輯是否成功。理想情況下,我們期望客戶端對成功的 HTTP 請求能夠直接將響應體解析為需要的 Go 結構體,并進行后續的業務邏輯處理,而不用再判斷請求是否成功。

方式二:失敗返回,返回體中返回錯誤信息

Twitter API 的錯誤返回設計會根據錯誤類型返回對應的 HTTP 狀態碼,并在返回體中返回錯誤信息和自定義業務錯誤碼。成功的業務請求則返回 200 HTTP 狀態碼。例如:

HTTP/1.1 400 Bad Request
x-connection-hash: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
set-cookie: guest_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Date: Thu, 01 Jun 2017 03: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."}]
}

方式二相比方式一,對于成功的請求不需要再次判錯。然而,方式二還可以進一步優化:整數格式的業務錯誤碼 215 可讀性較差,用戶無法從 215 直接獲取任何有意義的信息。建議將其替換為語義化的字符串,例如:NotFound.PostNotFound

Twitter API 返回的錯誤是一個數組,在實際開發獲取錯誤時,需要先判斷數組是否為空,如不為空,再從數組中獲取錯誤,開發復雜度較高。建議采用更簡單的錯誤返回格式:

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

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

fastgo 錯誤返回設計和實現

fastgo 項目錯誤返回格式采用了方式二,在接口失敗時返回對應的 HTTP/gRPC 狀態碼,并在返回體中返回具體的錯誤信息,例如:

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

制定錯誤碼規范

錯誤碼是直接暴露給用戶的,因此需要設計一個易讀、易懂且規范化的錯誤碼。在設計錯誤碼時可以根據實際需求自行設計,也可以參考其他優秀的設計方案。

一般來說,當調研某項技術實現時,建議優先參考各大公有云廠商的實現方式,例如騰訊云、阿里云、華為云等。這些公有云廠商直接面向企業和個人,專注于技術本身,擁有強大的技術團隊,因此它們的設計與實現具有很高的參考價值。

經過調研,此處采用了騰訊云 API 3.0 的錯誤碼設計規范。騰訊云采用了兩級錯誤碼設計。以下是兩級錯誤碼設計相較于簡單錯誤碼(如 215、InvalidParameter)的優勢:

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

fastgo 項目預定義了一些錯誤碼,這些錯誤碼位于以下 3 個文件中:

  • internal/pkg/errorsx/code.go:定義了一些通用的錯誤碼;
  • internal/pkg/errorsx/user.go:定義了用戶相關的錯誤碼;
  • internal/pkg/errorsx/post.go:定義了博客相關的錯誤碼。

一些錯誤碼舉例解釋如下:

錯誤碼錯誤描述錯誤類型
OK請求成功-
InternalError內部錯誤1
NotFound資源不存在0

上表中,錯誤類型 0 代表客戶端錯誤,1 代表服務端錯誤,2 代表客戶端錯誤/服務端錯誤,- 代表請求成功。

fastgo 錯誤包設計

為了避免與標準庫的 errors 包命名沖突,fastgo 項目的錯誤包命名為 errorsx,寓意為“擴展的錯誤處理包”。

由于 fastgo 項目的錯誤包命名為 errorsx,為保持命名一致性,定義了一個名為 ErrorX 的結構體,用于描述錯誤信息,具體定義如下:

// ErrorX 定義了 OneX 項目體系中使用的錯誤類型,用于描述錯誤的詳細信息.
type ErrorX struct {// Code 表示錯誤的 HTTP 狀態碼,用于與客戶端進行交互時標識錯誤的類型.Code int `json:"code,omitempty"`// Reason 表示錯誤發生的原因,通常為業務錯誤碼,用于精準定位問題.Reason string `json:"reason,omitempty"`// Message 表示簡短的錯誤信息,通常可直接暴露給用戶查看.Message string `json:"message,omitempty"`
}

ErrorX 是一個錯誤類型,因此需要實現 Error 方法:

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

Error() 返回的錯誤信息中,包含了 HTTP 狀態碼、錯誤發生的原因、錯誤信息。通過這些詳盡的錯誤信息返回,幫助開發者快速定位錯誤。

在 Go 項目開發中,發生錯誤的原因有很多,大多數情況下,開發者希望將真實的錯誤信息返回給用戶。因此,還需要提供一個方法用來設置 ErrorX 結構體中的 Message 字段。為了滿足上述訴求,給 ErrorX 增加 WithMessage方法。實現方式如下是代碼所示(位于文件 internal/pkg/errorsx/errorsx.go 中):

// ErrorX 定義了 fastgo 項目中使用的錯誤類型,用于描述錯誤的詳細信息.
type ErrorX struct {// Code 表示錯誤的 HTTP 狀態碼,用于與客戶端進行交互時標識錯誤的類型.Code int `json:"code,omitempty"`// Reason 表示錯誤發生的原因,通常為業務錯誤碼,用于精準定位問題.Reason string `json:"reason,omitempty"`// Message 表示簡短的錯誤信息,通常可直接暴露給用戶查看.Message string `json:"message,omitempty"`
}// New 創建一個新的錯誤.
func New(code int, reason string, format string, args ...any) *ErrorX {return &ErrorX{Code:    code,Reason:  reason,Message: fmt.Sprintf(format, args...),}
}// Error 實現 error 接口中的 `Error` 方法.
func (err *ErrorX) Error() string {return fmt.Sprintf("error: code = %d reason = %s message = %s", err.Code, err.Reason, err.Message)
}// WithMessage 設置錯誤的 Message 字段.
func (err *ErrorX) WithMessage(format string, args ...any) *ErrorX {err.Message = fmt.Sprintf(format, args...)return err
}

在 Go 項目開發中,通常需要將一個 error 類型的錯誤 err,解析為 *ErrorX 類型,并獲取 *ErrorX 中的 Code 字段和 Reason 字段的值。Code 字段可用來設置 HTTP 狀態碼,Reason 字段可用來判斷錯誤類型。為此,errorsx 包實現了 FromError 方法,具體實現如下所示。

// FromError 嘗試將一個通用的 error 轉換為自定義的 *ErrorX 類型.
func FromError(err error) *ErrorX {// 如果傳入的錯誤是 nil,則直接返回 nil,表示沒有錯誤需要處理.if err == nil {return nil}// 檢查傳入的 error 是否已經是 ErrorX 類型的實例.// 如果錯誤可以通過 errors.As 轉換為 *ErrorX 類型,則直接返回該實例.if errx := new(ErrorX); errors.As(err, &errx) {return errx}// 默認返回未知錯誤錯誤. 該錯誤代表服務端出錯return New(ErrInternal.Code, ErrInternal.Reason, err.Error())
}

fastgo 錯誤碼定義

在實現了 errorsx 錯誤包之后,便可以根據需要預定義項目需要的錯誤。這些錯誤,可以在代碼中便捷的引用。通過直接引用預定義錯誤,不僅可以提高開發效率,還可以保持整個項目的錯誤返回是一致的。

fastgo 的預定義錯誤定義在 internal/pkg/errorsx 目錄下。一些基礎錯誤定義如下:

// errorsx 預定義標準的錯誤.
var (// OK 代表請求成功.OK = &ErrorX{Code: http.StatusOK, Message: ""}// ErrInternal 表示所有未知的服務器端錯誤.ErrInternal = &ErrorX{Code: http.StatusInternalServerError, Reason: "InternalError", Message: "Internal server error."}// ErrNotFound 表示資源未找到.ErrNotFound = &ErrorX{Code: http.StatusNotFound, Reason: "NotFound", Message: "Resource not found."}
)

更完整的預定義錯誤,可直接查看 master 分支中,internal/pkg/errorsx 中的錯誤定義文件。

fastgo 錯誤返回規范

為了標準化接口錯誤返回,fastgo 項目提供了通用的接口返回函數,該函數可以解析錯誤,并返回固定的錯誤返回格式。實現代碼位于 internal/pkg/core/core.go 文件中,代碼內容如下:

package coreimport ("net/http""github.com/gin-gonic/gin""github.com/onexstack/fastgo/internal/pkg/errorsx"
)// ErrorResponse 定義了錯誤響應的結構,
// 用于 API 請求中發生錯誤時返回統一的格式化錯誤信息.
type ErrorResponse struct {// 錯誤原因,標識錯誤類型Reason string `json:"reason,omitempty"`// 錯誤詳情的描述信息Message string `json:"message,omitempty"`
}// WriteResponse 是通用的響應函數.
// 它會根據是否發生錯誤,生成成功響應或標準化的錯誤響應.
func WriteResponse(c *gin.Context, err error, data any) {if err != nil {// 如果發生錯誤,生成錯誤響應errx := errorsx.FromError(err) // 提取錯誤詳細信息c.JSON(errx.Code, ErrorResponse{Reason:  errx.Reason,Message: errx.Message,})return}// 如果沒有錯誤,返回成功響應c.JSON(http.StatusOK, data)
}

上述代碼,定義了一個通用的錯誤返回結構體:ErrorResponse。ErrorResponse 中包含了錯誤返回的原因和消息。

在 API 接口返回時,會調用 WriteResponse 函數。WriteResponse 函數會判斷是否發生了錯誤,如果發生了錯誤,會解析錯誤為 errorsx.ErrorX 類型的錯誤,并從中獲取 CodeReason 字段,并設置給ErrorResponse 類型的變量。如果沒有發生錯誤,直接返回自定義數據。

返回統一的錯誤格式

上面,我們開發了錯誤包、自定義了錯誤返回碼,并提供了接口返回函數 WriteResponse。接下來,就可以使用 WriteResponse 來返回接口數據。

更新 internal/apiserver/server.go 文件中的 NoRoute 返回實現和 /healthz 接口的返回實現。代碼變更如下:

package apiserverimport (..."github.com/onexstack/fastgo/internal/pkg/core""github.com/onexstack/fastgo/internal/pkg/errorsx"...
)
...
// NewServer 根據配置創建服務器.
func (cfg *Config) NewServer() (*Server, error) {...// 注冊 404 Handler.engine.NoRoute(func(c *gin.Context) {core.WriteResponse(c, errorsx.ErrNotFound.WithMessage("Page not found"), nil)})// 注冊 /healthz handler.engine.GET("/healthz", func(c *gin.Context) {core.WriteResponse(c, nil, map[string]string{"status": "ok"})})...
}

上述代碼通過調用 WriteResponse 返回了標準的錯誤返回。并且在 NoRoute 路由函數中,還指定了要返回的自定義錯誤碼 ErrNotFound,并給 ErrNotFound 設置了自定義返回消息。

編譯并測試

運行以下命令編譯并測試錯誤返回功能:

$ ./build.sh 
$ _output/fg-apiserver -c configs/fg-apiserver.yaml

打開一個新的 Linux 終端,執行以下命令:

$ curl http://127.0.0.1:6666/noroute
{"reason":"NotFound","message":"Page not found"}

可以看到,當我們方位一個不存在的路徑時,返回了自定義錯誤碼及自定義消息。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/897623.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/897623.shtml
英文地址,請注明出處:http://en.pswp.cn/news/897623.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

DeepSeek結合Mermaid繪圖(流程圖、時序圖、類圖、狀態圖、甘特圖、餅圖)轉載

思維速覽&#xff1a; 本文將詳細介紹如何利用DeepSeek結合Mermaid語法繪制各類專業圖表&#xff0c;幫助你提高工作效率和文檔質量。 ▍DeepSeek入門使用請看&#xff1a;deepseek保姆級入門教程&#xff08;網頁端使用 本地客戶端部署 使用技巧&#xff09; DeepSeek官網…

Java靜態變量與PHP靜態變量的對比

Java的靜態變量在多線程并發的情況下是線程共有的。以下是關鍵點總結&#xff1a; 存儲位置&#xff1a;靜態變量屬于類&#xff0c;存儲在方法區&#xff08;或元空間&#xff09;&#xff0c;這是所有線程共享的內存區域。因此&#xff0c;所有線程訪問的都是同一個靜態變量實…

c++20 Concepts的簡寫形式與requires 從句形式

c20 Concepts的簡寫形式與requires 從句形式 原始寫法&#xff08;簡寫形式&#xff09;等效寫法&#xff08;requires 從句形式&#xff09;關鍵區別說明&#xff1a;組合多個約束的示例&#xff1a;兩種形式的編譯結果&#xff1a;更復雜的約束示例&#xff1a;標準庫風格的約…

上下分層、左右分離的驅動設計思想

之前了解了最簡單的驅動程序、但是不易擴展、現在繼續學習、上下分層、左右分離的驅動設計思想。 1、led_dev.c函數 上層函數&#xff0c;①定義一個結構體&#xff0c;存儲函數用來接應app的函數。②定義一個入口函數&#xff0c;將我們接應的函數告訴內核&#xff0c;給這個…

人工智能在醫療領域的應用:技術革新與未來展望

人工智能&#xff08;AI&#xff09;技術正在重塑醫療行業的面貌。從輔助診斷到藥物研發&#xff0c;從健康管理到手術機器人&#xff0c;AI的廣泛應用不僅提升了醫療效率&#xff0c;還為精準醫療和個性化治療提供了新可能。根據2025年多份研究報告及政策文件&#xff0c;全球…

《歷史代碼分析》5、動態控制列表的列

?? 本系列《歷史代碼分析》為工作中遇到具有代表性的代碼。今天我們講一下&#xff0c;動態展示列表的列&#xff0c;因為找不到代碼了&#xff0c;所有本篇用圖展示。 舉個栗子 ?? 我們希望能夠動態的控制列表的列&#xff0c;例如&#xff0c;英語老師只想知道自己學…

Windows HD Video Converter Factory PRO-v27.9.0-

Windows HD Video Converter Factory PRO 鏈接&#xff1a;https://pan.xunlei.com/s/VOL9TaiuS7rXbu-1kEDndoceA1?pwd7qch# 支持300多種視頻格式轉換&#xff0c;在保留視頻質量的同時&#xff0c;壓縮率可達80%&#xff0c;轉換速度可達50X速率&#xff01; 支持畫面剪切、片…

C++程序設計語言筆記——抽象機制:構造、清理、拷貝和移動

0 應該將構造函數、賦值操作以及析構函數設計為一組匹配的操作。 在C中&#xff0c;構造函數、賦值操作符和析構函數共同管理對象的資源生命周期。為確保資源安全且一致地處理&#xff0c;需將它們作為一組匹配的操作設計。以下是關鍵要點&#xff1a; 為何需要協同設計&…

##Hive安裝-初始化元數據報錯 *** schemaTool failed ***

報錯&#xff1a; org.apache.hadoop.hive.metastore.HiveMetaException: Failed to get schema version. Underlying cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException : Communications link failure 解決方案&#xff1a; 嘗試一&#xff1a;javax.jdo.o…

遠程手機遙控開關原理及應用

遠程手機遙控開關的工作原理主要是通過互聯網傳遞無線信號&#xff0c;控制用電器的一種智能家居產品。 遠程手機遙控開關的基本套件包括&#xff1a;手機APP、網線、家用WIFI中轉無服務器或者是工廠提供的自帶網線端口的中轉服務器、連接用電器的接收器。使用時&#xff0c;手…

Mac java全棧開發環境配置

前言 由于最近手中的windows本子壞了,所以搞了一臺m系列的macbookpro 作為一個開發者 面對新設備最先考慮的應該就是各種sdk、中間件服務、環境變量配置和工具了吧!!! 本文將帶你手把手學習Mac搭建屬于自己的本地開發環境 安裝brew 什么是brew? ?Brew(全稱Homebrew)…

Ubuntu conda虛擬環境不同設備之間遷移

Ubuntu conda環境遷移&#xff08;conda-pack&#xff09; 方法一&#xff1a;壓縮拷貝方法二&#xff1a;conda-pack 在一臺電腦配置好conda虛擬環境后&#xff0c;若在其它電腦需要同樣的環境&#xff0c;可通過如下兩種方式進行遷移。 方法一&#xff1a;壓縮拷貝 找到Ubu…

詳細學習 pandas 和 xlrd:從零開始

詳細學習 pandas 和 xlrd&#xff1a;從零開始 前言 在數據處理和分析中&#xff0c;Excel 文件是最常見的數據格式之一。Python 提供了強大的庫 pandas&#xff0c;可以輕松地處理 Excel 文件中的數據。同時&#xff0c;我們還可以使用 xlrd 來讀取 Excel 文件&#xff0c;尤…

HTMLCSS繪制三角形

1.代碼&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>01triangle</title><s…

vue3-element-admin 前后端本地啟動聯調

一、后端環境準備 1.1、下載地址 gitee 下載地址 1.2、環境要求 JDK 17 1.3、項目啟動 克隆項目 git clone https://gitee.com/youlaiorg/youlai-boot.git數據庫初始化 執行 youlai_boot.sql 腳本完成數據庫創建、表結構和基礎數據的初始化。 修改配置 application-dev.y…

C++中error C2027: 使用了未定義類型 問題部分解決方法

在 C 編程中&#xff0c;遇到錯誤 C2027&#xff1a;“使用了未定義類型”通常意味著在代碼中使用了某種類型&#xff0c;但是編譯器無法識別這個類型的定義。這個錯誤通常有幾個常見的原因&#xff1a; 1. 缺少包含頭文件 如果使用了某個庫中的類型&#xff0c;但是沒有包含…

WinForm模態與非模態窗體

1、模態窗體 1&#xff09;定義&#xff1a; 模態窗體是指當窗體顯示時&#xff0c;用戶必須先關閉該窗體&#xff0c;才能繼續與應用程序的其他部分進行交互。 2&#xff09;特點&#xff1a; 窗體以模態方式顯示時&#xff0c;會阻塞主窗體的操作。用戶必須處理完模態窗體上…

Agisoft Metashape 創建分塊建模

Agisoft Metashape 創建分塊建模 文章目錄 Agisoft Metashape 創建分塊建模前言一、構建分塊模型1.1、設置模型范圍1.2、參數設置二、構建紋理三、導出分塊模型3.1整體導出3.2單獨導出選定的分塊四、編輯分塊模型前言 從 Agisoft Metashape Professional 的 2.1. 版本開始,就…

golang從入門到做牛馬:第二十二篇-Go語言并發:多任務的“協同作戰”

在Go語言中,并發是一種強大的編程范式,允許程序同時執行多個任務。Go通過goroutines和channels提供了一種簡潔且高效的方式來實現并發。此外,Go的調度器(Scheduler)基于GMP模型,能夠高效地管理并發。接下來,讓我們一起深入了解Go語言中的并發機制。 Goroutines:輕量級的…

MinIO的預簽名直傳機制

我們傳統使用MinIo做OSS對象存儲的應用方式往往都是在后端配置與MinIO的連接和文件上傳下載的相關接口&#xff0c;然后我們在前端調用這些接口完成文件的上傳下載機制&#xff0c;但是&#xff0c;當并發量過大&#xff0c;頻繁訪問會對后端的并發往往會對服務器造成極大的壓力…