go語言規(guī)范RESTful?API業(yè)務錯誤處理
錯誤碼
現(xiàn)如今,主流的 Web API 都采用 RESTful 設計風格,對于接口返回的 HTTP 狀態(tài)碼和響應內容都有統(tǒng)一的規(guī)范。針對接口錯誤響應,一般都會返回一個 Code(錯誤碼)和 Message(錯誤消息內容),通常錯誤碼 Code 用來定位一個唯一的錯誤,錯誤消息 Message 用來展示錯誤信息。
本文就來詳細介紹下,如何將 RESTful API 的錯誤處理進行規(guī)范化。
為什么需要業(yè)務錯誤碼
雖然 RESTful API 能夠通過 HTTP 狀態(tài)碼來標記一個請求的成功或失敗,但 HTTP 狀態(tài)碼作為一個通用的標準,并不能很好的表達業(yè)務錯誤。
比如一個 500 的錯誤響應,可能是由后端數(shù)據(jù)庫連接異常引起的、也可能由內部代碼邏輯錯誤引起,這些都無法通過 HTTP 狀態(tài)碼感知到,如果程序出現(xiàn)錯誤,不方便開發(fā)人員 Debug。
因此我們有必要設計一套用來標識業(yè)務錯誤的錯誤碼,這有別于 HTTP 狀態(tài)碼,是跟系統(tǒng)具體業(yè)務息息相關的。
錯誤碼功能
在設計錯誤碼之前,我們需要明確下錯誤碼應該具備哪些屬性,以滿足業(yè)務需要。
- 錯誤碼必須是唯一的。只有錯誤碼是唯一的才方便在程序出錯時快速定位問題,不然程序出錯,返回錯誤碼不唯一,想要根據(jù)錯誤碼排查問題,就要針對這一錯誤碼所表示的錯誤列表進行逐一排查。
- 錯誤碼需要是可閱讀的。意思是說,通過錯誤碼,我們就能快速定位到是系統(tǒng)的哪個組件出現(xiàn)了錯誤,并且知道錯誤的類型,不然也談不上叫「業(yè)務錯誤碼」了。一個清晰可讀的錯誤碼在微服務系統(tǒng)中定位問題尤其有效。
- 通過錯誤碼能夠方便知道 HTTP 狀態(tài)碼。這一點往往容易被人忽略,不過我比較推薦這種做法,因為在 Review 代碼時,通過返回錯誤碼,就能很容易知道接口返回 HTTP 狀態(tài)碼,這不僅方便理解代碼,更方便錯誤的統(tǒng)一處理。
錯誤碼設計
錯誤碼調研
錯誤碼的設計我們可以參考業(yè)內使用量比較大的開放 API 設計,比較有代表性的是阿里云和新浪網的開放 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(當前請求唯一標識)、HostId(Host 唯一標識)、Recommend(錯誤診斷地址),可以說這個錯誤信息非常全面了。
再來看下新浪網開放 API 錯誤返回結果的設計:
{
"request": "/statuses/home_timeline.json",
"error_code": "20502",
"error": "Need you follow uid."
}
相比阿里云,新浪網的錯誤返回更簡潔一些。其中 request 為請求路徑,error_code 即為錯誤碼 Code,error 則表示錯誤信息 Message。
錯誤代碼 20502 說明如下:
| 2 | 05 | 02 |
|---|---|---|
| 服務級錯誤(1為系統(tǒng)級錯誤) | 服務模塊代碼 | 具體錯誤代碼 |
新浪網的錯誤碼為數(shù)字類型的字符串,相比阿里云的錯誤碼要簡短不少,并且對程序更加友好,也是我個人更推薦的設計。
業(yè)務錯誤碼
結合市面上這些優(yōu)秀的開放 API 錯誤碼設計,以及我在實際開發(fā)中的工作總結,我設計的錯誤碼規(guī)則如下:
業(yè)務錯誤碼由 8 位純數(shù)字組成,類型為 int。
業(yè)務錯誤碼示例格式:40001002。
錯誤碼說明:
| 1-3 位 | 4-5 位 | 6-8 位 |
|---|---|---|
| 400 | 01 | 002 |
| HTTP 狀態(tài)碼 | 組件編號 | 組件內部錯誤碼 |
錯誤碼設計為純數(shù)字主要是為了程序中使用起來更加方便,比如根據(jù)錯誤碼計算 HTTP 狀態(tài)碼,只需要通過簡單的數(shù)學取模計算就能做到。
使用兩位數(shù)字來標記不同組件,最多能表示 99 個組件,即使項目全部采用微服務開發(fā),一般來說也是足夠用的。
最后三位代表組件內部錯誤碼,最多能表示 1000 個錯誤。其實通常來說一個組件內部是用不上這么多錯誤的,如果組件較小,完全可以設計成兩位數(shù)字。
另外,有些廠商中還會設計一些公共的錯誤碼,可以稱為「全局錯誤碼」,這些錯誤碼在各組件間通用,以此來減少定義重復錯誤。在我們的錯誤碼設計中,可以將組件編號為 00 的標記為全局錯誤碼,其他組件編號從 01 開始。
錯誤格式
有了錯誤碼,還需要定義錯誤響應格式,設計一個標準的 API 錯誤響應格式如下:
{
"code": 50000000,
"message": "系統(tǒng)錯誤",
"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}
code 即為錯誤碼,message 為錯誤信息,reference 則是錯誤文檔地址,用來告知用戶如何解決這個錯誤,對標的是阿里云錯誤響應中的 Recommend 字段。
錯誤碼實現(xiàn)
因為每一個錯誤碼和錯誤信息以及錯誤文檔地址都是一一對應的,所以我們需要一個對象來保存這些信息,在 Go 中可以使用結構體。
可以設計如下結構體:
type apiCode struct {
code int
msg string
ref string
}
這是一個私有結構體,外部項目要想使用,則需要一個構造函數(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ù),如果不傳則默認為空。
NewAPICode 返回值 APICoder 是一個接口,這在 Go 中是一種慣用做法。通過接口可以解耦,方便依賴 apiCode 的代碼編寫測試,用戶可以對 APICoder 進行 Mock;另一方面,我們稍后會為 apiCode 實現(xiàn)對應的錯誤包,使用接口來表示錯誤碼可以方便用戶定義自己的 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)建錯誤碼結構體對象:
var ( CodeBadRequest = NewAPICode(40001001, "請求不合法") CodeUnknownError = NewAPICode(50001001, "系統(tǒng)錯誤", "https://github.com/jianghushinian/gokit/tree/main/errors") )
錯誤包
設計好了錯誤碼,并不能直接使用,我們還需要一個與之配套的錯誤包來簡化錯誤碼的使用。
錯誤包功能
錯誤包要能夠完美支持上面設計的錯誤碼。所以需要使用 APICoder 來構造錯誤對象。
錯誤包應該能夠查看原始錯誤原因。這就需要實現(xiàn) Unwrap 方法,Wrap/Unwrap 方法是在 Go 1.13 中被加入進 errors 包的,目的是能夠處理嵌套錯誤。
錯誤包應該能夠支持對內對外展示不同信息。這就需要實現(xiàn) Format 方法,根據(jù)需要可以將錯誤格式化成不同輸出。
錯誤包應該能夠支持展示堆棧信息。這對 Debug 來說相當重要,也是 Go 自帶的 errors 包不足的地方。
為了方便在日志中記錄結構化錯誤信息,錯誤包還要能夠支持 JSON 序列化。這需要實現(xiàn) MarshalJSON/UnmarshalJSON 兩個方法。
錯誤包設計
一個錯誤對象結構體設計如下:
type apiError struct {
coder APICoder
cause error
*stack
}
其中 coder 用來保存實現(xiàn)了 APICoder 接口的對象,cause 用來記錄錯誤原因,stack 用來展示錯誤堆棧。
錯誤對象的構造函數(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ù)為一個可選的錯誤原因。
其實構造一個錯誤對象也就是對一個錯誤進行 Wrap 的過程,所以我還為構造函數(shù) NewAPIError 定義了一個別名 WrapC,表示使用錯誤碼將一個錯誤包裝成一個新的錯誤。
一個錯誤對象必須要實現(xiàn) Error 方法:
func (a *apiError) Error() string {
return fmt.Sprintf("[%d] - %s", a.coder.Code(), a.coder.Message())
}
默認情況下,獲取到的錯誤內容只包含錯誤碼 Code 和錯誤信息 Message。
為了方便獲取被包裝錯誤的原始錯誤,還要實現(xiàn) Unwrap 方法:
func (a *apiError) Unwrap() error {
return a.cause
}
為了能在打印或寫入日志時展示不同信息,則要實現(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 | 錯誤碼、錯誤信息、錯誤原因、錯誤堆棧 |
| %#v | JSON 格式的 錯誤碼、錯誤信息、錯誤文檔地址、錯誤原因、錯誤堆棧 |
| %q | 在 錯誤碼、錯誤信息 外層增加了一個雙引號 |
這些錯誤格式基本上能滿足所有業(yè)務開發(fā)中的需求了,如果還有其他格式需要,則可以在此基礎上進一步開發(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、Message、Reference(可選)三個字段,對內需要額外展示錯誤原因以及錯誤堆棧。所以 errorMessage 中 Reference、Cause、Stack 字段都帶有 omitempty 屬性,這樣在 MarshalJSON 時只會序列化 Code、Message、Reference 這三個字段。
至此,我們就實現(xiàn)了錯誤包的設計。
錯誤碼及錯誤包的使用
使用示例
通過上面的講解,我們了解了錯誤碼和錯誤包的設計規(guī)范,接下來看看如何使用它們。這里以錯誤碼及錯誤包在 Gin 中的使用為例進行講解。
使用 Gin 構建一個簡單的 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 內部程序執(zhí)行成功返回 ResponseOK(c, account),失敗則返回 ResponseError(c, err)。
在處理返回失敗的響應時,都會通過 apierr.WrapC 或 apierr.NewAPIError 將底層函數(shù)返回的初始錯誤進行一層包裝,根據(jù)錯誤級別,包裝成不同的錯誤碼進行返回。
其中 ResponseOK 和 ResponseError 定義如下:
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) 來記錄錯誤日志,在其內部使用 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 中一個正常的返回結果是什么樣,使用 cURL 來發(fā)送一個請求:curl http://localhost:8080/accounts/1。
客戶端得到如下響應結果:
{
"id": 1,
"name": "account_1"
}
服務端打印正常的請求日志:

再來測試下請求一個不存在的賬號:curl http://localhost:8080/accounts/12。
客戶端得到如下響應結果:
{
"code": 40401001,
"message": "資源未找到"
}
返回結果中沒有 reference 字段,是因為對于 reference 為空的情況,在 JSON 序列化過程中會被隱藏。
服務端打印的錯誤日志如下:
========== 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 的錯誤響應:curl http://localhost:8080/accounts/500。
客戶端得到如下響應結果:
{
"code": 50001001,
"message": "系統(tǒng)錯誤",
"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}
這次得到一個帶有 reference 字段的完整錯誤響應。
服務端打印的錯誤日志如下:
========== 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 格式的結構化日志。
以上便是我們設計的錯誤碼及錯誤包在實際開發(fā)場景中的應用。
使用建議
根據(jù)我的經驗,總結了一些錯誤碼及錯誤包的使用建議,現(xiàn)在將其分享給你。
使用盡量少的 HTTP 狀態(tài)碼
HTTP 狀態(tài)碼大概分為 5 大類,分別是 1XX、2XX、3XX、4XX、5XX。根據(jù)我的實際工作經驗,我們并不會使用全部的狀態(tài)碼,最常用的狀態(tài)碼不超過 10 個。
所以即使我們設計的業(yè)務錯誤碼支持攜帶 HTTP 狀態(tài)碼,但也不推薦使用過多的 HTTP 狀態(tài)碼,以免加重前端工作量。
推薦在錯誤碼中使用的 HTTP 狀態(tài)碼如下:
- 400: 請求不合法
- 401: 認證失敗
- 403: 授權失敗
- 404: 資源未找到
- 500: 系統(tǒng)錯誤
其中 4XX 代表客戶端錯誤,而如果是服務端錯誤,則統(tǒng)一使用 500 狀態(tài)碼,具體錯誤原因可以通過業(yè)務錯誤碼定位。
使用中間件來記錄錯誤日志
由于我們設計的錯誤包支持 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ù)功能。
總結
本篇文章講解了如何設計一個規(guī)范的錯誤碼以及與之配套的錯誤包。
我參考了一些開源的 API 錯誤碼設計方案,并結合我自己的實際工作經驗,給出了我認為比較合理的錯誤碼設計方案。
同時也針對這個錯誤碼方案,設計了一個配套的錯誤包,來簡化使用過程,并給出了我的一些使用建議。
錯誤包中記錄錯誤堆棧部分的代碼參考了 pkg/errors 包實現(xiàn),感興趣的同學可以點擊進去進行進一步學習。
本文完整代碼實現(xiàn)我放在了 Github 上,供你參考使用。
以上就是go語言規(guī)范RESTful API業(yè)務錯誤處理的詳細內容,更多關于go RESTful API錯誤處理的資料請關注腳本之家其它相關文章!
相關文章
???????Golang實現(xiàn)RabbitMQ中死信隊列幾種情況
本文主要介紹了???????Golang實現(xiàn)RabbitMQ中死信隊列幾種情況,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-03-03
golang channel讀取數(shù)據(jù)的幾種情況
本文主要介紹了golang channel讀取數(shù)據(jù)的幾種情況,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-02-02
Go源碼字符串規(guī)范檢查lint工具strchecker使用詳解
這篇文章主要為大家介紹了Go源碼字符串規(guī)范檢查lint工具strchecker使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06
golang中http請求的context傳遞到異步任務的坑及解決
這篇文章主要介紹了golang中http請求的context傳遞到異步任務的坑及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03

