提示:
- 所有體系課見專欄:Go 項目開發極速入門實戰課;
- 歡迎加入 云原生 AI 實戰 星球,12+ 高質量體系課、20+ 高質量實戰項目助你在 AI 時代建立技術競爭力(聚焦于 Go、云原生、AI Infra);
- 本節課最終源碼位于 fastgo 項目的 feature/s10 分支;
- 更詳細的課程版本見:Go 項目開發中級實戰課:16 | 基礎 Go 包開發:錯誤返回設計和實現
在 Go 項目開發中,為了方便客戶端處理返回,排查錯誤,還需要實現統一的錯誤返回。統一的錯誤返回包括以下 2 個方面:
- 錯誤格式統一:返回統一的錯誤格式,方便客戶端解析,并獲取錯誤;
- 自定義業務錯誤碼:HTTP 的錯誤碼有限,并且不適合業務錯誤碼。所以,在實際開發中,還需要自定義業務錯誤碼。
為了實現統一的錯誤返回,接下來還需要實現錯誤包和自定義錯誤碼。
錯誤返回方法
先來看下錯誤返回的方式。在 Go 項目開發中,錯誤的返回方式通常有以下兩種:
- 始終返回 HTTP 200 狀態碼,并在 HTTP 返回體中返回錯誤信息;
- 返回 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
類型的錯誤,并從中獲取 Code
和 Reason
字段,并設置給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"}
可以看到,當我們方位一個不存在的路徑時,返回了自定義錯誤碼及自定義消息。