如何规范 RESTful API 的业务错误处理

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 如何规范 RESTful API 的业务错误处理

现如今,主流的 Web API 都采用 RESTful 设计风格,对于接口返回的 HTTP 状态码和响应内容都有统一的规范。针对接口错误响应,一般都会返回一个 Code(错误码)和 Message(错误消息内容),通常错误码 Code 用来定位一个唯一的错误,错误消息 Message 用来展示错误信息。

本文就来详细介绍下,如何将 RESTful API 的错误处理进行规范化。

错误码

为什么需要业务错误码

虽然 RESTful API 能够通过 HTTP 状态码来标记一个请求的成功或失败,但 HTTP 状态码作为一个通用的标准,并不能很好的表达业务错误。

比如一个 500 的错误响应,可能是由后端数据库连接异常引起的、也可能由内部代码逻辑错误引起,这些都无法通过 HTTP 状态码感知到,如果程序出现错误,不方便开发人员 Debug。

因此我们有必要设计一套用来标识业务错误的错误码,这有别于 HTTP 状态码,是跟系统具体业务息息相关的。

错误码功能

在设计错误码之前,我们需要明确下错误码应该具备哪些属性,以满足业务需要。

  • 错误码必须是唯一的。只有错误码是唯一的才方便在程序出错时快速定位问题,不然程序出错,返回错误码不唯一,想要根据错误码排查问题,就要针对这一错误码所表示的错误列表进行逐一排查。
  • 错误码需要是可阅读的。意思是说,通过错误码,我们就能快速定位到是系统的哪个组件出现了错误,并且知道错误的类型,不然也谈不上叫「业务错误码」了。一个清晰可读的错误码在微服务系统中定位问题尤其有效。
  • 通过错误码能够方便知道 HTTP 状态码。这一点往往容易被人忽略,不过我比较推荐这种做法,因为在 Review 代码时,通过返回错误码,就能很容易知道接口返回 HTTP 状态码,这不仅方便理解代码,更方便错误的统一处理。

错误码设计

错误码调研

错误码的设计我们可以参考业内使用量比较大的开放 API 设计,比较有代表性的是阿里云和新浪网的开放 API。

如以下是一个阿里云 ECS 接口错误的返回:

1
2
3
4
5
6
7
{
"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"
}

可以发现,Code 和 Message 都为字符串类型,并且还有 RequestId(当前请求唯一标识)、HostId(Host 唯一标识)、Recommend(错误诊断地址),可以说这个错误信息非常全面了。

再来看下新浪网开放 API 错误返回结果的设计:

1
2
3
4
5
{
"request": "/statuses/home_timeline.json",
"error_code": "20502",
"error": "Need you follow uid."
}

相比阿里云,新浪网的错误返回更简洁一些。其中 request 为请求路径,error_code 即为错误码 Code,error 则表示错误信息 Message。

错误代码 20502 说明如下:

2 05 02
服务级错误(1为系统级错误) 服务模块代码 具体错误代码

新浪网的错误码为数字类型的字符串,相比阿里云的错误码要简短不少,并且对程序更加友好,也是我个人更推荐的设计。

业务错误码

结合市面上这些优秀的开放 API 错误码设计,以及我在实际开发中的工作总结,我设计的错误码规则如下:

  • 业务错误码由 8 位纯数字组成,类型为 int
  • 业务错误码示例格式:40001002
  • 错误码说明:
1-3 位 4-5 位 6-8 位
400 01 002
HTTP 状态码 组件编号 组件内部错误码

错误码设计为纯数字主要是为了程序中使用起来更加方便,比如根据错误码计算 HTTP 状态码,只需要通过简单的数学取模计算就能做到。

使用两位数字来标记不同组件,最多能表示 99 个组件,即使项目全部采用微服务开发,一般来说也是足够用的。

最后三位代表组件内部错误码,最多能表示 1000 个错误。其实通常来说一个组件内部是用不上这么多错误的,如果组件较小,完全可以设计成两位数字。

另外,有些厂商中还会设计一些公共的错误码,可以称为「全局错误码」,这些错误码在各组件间通用,以此来减少定义重复错误。在我们的错误码设计中,可以将组件编号为 00 的标记为全局错误码,其他组件编号从 01 开始。

错误格式

有了错误码,还需要定义错误响应格式,设计一个标准的 API 错误响应格式如下:

1
2
3
4
5
{
"code": 50000000,
"message": "系统错误",
"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}

code 即为错误码,message 为错误信息,reference 则是错误文档地址,用来告知用户如何解决这个错误,对标的是阿里云错误响应中的 Recommend 字段。

错误码实现

因为每一个错误码和错误信息以及错误文档地址都是一一对应的,所以我们需要一个对象来保存这些信息,在 Go 中可以使用结构体。

可以设计如下结构体:

1
2
3
4
5
type apiCode struct {
	code int
	msg  string
	ref  string
}

这是一个私有结构体,外部项目要想使用,则需要一个构造函数:

1
2
3
4
5
6
7
8
9
10
11
funcNewAPICode(code int, message string, reference ...string)APICoder {
	ref := ""
iflen(reference) > 0 {
		ref = reference[0]
	}
return &apiCode{
		code: code,
		msg:  message,
		ref:  ref,
	}
}

其中 reference 被设计为可变参数,如果不传则默认为空。

NewAPICode 返回值 APICoder 是一个接口,这在 Go 中是一种惯用做法。通过接口可以解耦,方便依赖 apiCode 的代码编写测试,用户可以对 APICoder 进行 Mock;另一方面,我们稍后会为 apiCode 实现对应的错误包,使用接口来表示错误码可以方便用户定义自己的 apiCode 类型。

为了便于使用,apiCode 提供了如下几个能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 接口接口的定义也就有了:

1
2
3
4
5
6
type APICoder interface {
	Code() int
	Message() string
	Reference() string
	HTTPStatus() int
}

apiCode 则实现了 APICoder 接口。

现在我们可以通过如下方式创建错误码结构体对象:

1
2
3
4
var (
	CodeBadRequest   = NewAPICode(40001001, "请求不合法")
	CodeUnknownError = NewAPICode(50001001, "系统错误", "https://github.com/jianghushinian/gokit/tree/main/errors")
)

错误包

设计好了错误码,并不能直接使用,我们还需要一个与之配套的错误包来简化错误码的使用。

错误包功能

  • 错误包要能够完美支持上面设计的错误码。所以需要使用 APICoder 来构造错误对象。
  • 错误包应该能够查看原始错误原因。这就需要实现 Unwrap 方法,Wrap/Unwrap 方法是在 Go 1.13 中被加入进 errors 包的,目的是能够处理嵌套错误。
  • 错误包应该能够支持对内对外展示不同信息。这就需要实现 Format 方法,根据需要可以将错误格式化成不同输出。
  • 错误包应该能够支持展示堆栈信息。这对 Debug 来说相当重要,也是 Go 自带的 errors 包不足的地方。
  • 为了方便在日志中记录结构化错误信息,错误包还要能够支持 JSON 序列化。这需要实现 MarshalJSON/UnmarshalJSON 两个方法。

错误包设计

一个错误对象结构体设计如下:

1
2
3
4
5
type apiError struct {
	coder APICoder
	cause error
	*stack
}

其中 coder 用来保存实现了 APICoder 接口的对象,cause 用来记录错误原因,stack 用来展示错误堆栈。

错误对象的构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var WrapC = NewAPIError
funcNewAPIError(coder APICoder, cause ...error)error {
var c error
iflen(cause) > 0 {
		c = cause[0]
	}
return &apiError{
		coder: coder,
		cause: c,
		stack: callers(),
	}
}

NewAPIError 通过 APICoder 来创建错误对象,第二个参数为一个可选的错误原因。

其实构造一个错误对象也就是对一个错误进行 Wrap 的过程,所以我还为构造函数 NewAPIError 定义了一个别名 WrapC,表示使用错误码将一个错误包装成一个新的错误。

一个错误对象必须要实现 Error 方法:

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

默认情况下,获取到的错误内容只包含错误码 Code 和错误信息 Message。

为了方便获取被包装错误的原始错误,还要实现 Unwrap 方法:

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

为了能在打印或写入日志时展示不同信息,则要实现 Format 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 在 错误码、错误信息 外层增加了一个双引号

这些错误格式基本上能满足所有业务开发中的需求了,如果还有其他格式需要,则可以在此基础上进一步开发 Format 方法。

用来进行 JSON 序列化和反序列化的 MarshalJSON/UnmarshalJSON 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
returnnil
}
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 只会返回 CodeMessageReference(可选)三个字段,对内需要额外展示错误原因以及错误堆栈。所以 errorMessageReferenceCauseStack 字段都带有 omitempty 属性,这样在 MarshalJSON 时只会序列化 CodeMessageReference 这三个字段。

至此,我们就实现了错误包的设计。

错误码及错误包的使用

使用示例

通过上面的讲解,我们了解了错误码和错误包的设计规范,接下来看看如何使用它们。这里以错误码及错误包在 Gin 中的使用为例进行讲解。

使用 Gin 构建一个简单的 Web Server 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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, "系统错误", "https://github.com/jianghushinian/gokit/tree/main/errors")
)
type Account struct {
	ID   int`json:"id"`
	Name string`json:"name"`
}
funcAccountOne(id int)(*Account, error) {
for _, v := range accounts {
if id == v.ID {
return &v, nil
		}
	}
// 模拟返回数据库错误
if id == 500 {
returnnil, ErrDatabase
	}
returnnil, ErrAccountNotFound
}
var accounts = []Account{
	{ID: 1, Name: "account_1"},
	{ID: 2, Name: "account_2"},
	{ID: 3, Name: "account_3"},
}
funcShowAccount(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)
}
funcmain() {
	r := gin.Default()
	r.GET("/accounts/:id", ShowAccount)
if err := r.Run(":8080"); err != nil {
panic(err)
	}
}

在这个 Web Server 中定义了一个 ShowAccount 函数,用来处理获取账号逻辑,在 ShowAccount 内部程序执行成功返回 ResponseOK(c, account),失败则返回 ResponseError(c, err)

在处理返回失败的响应时,都会通过 apierr.WrapCapierr.NewAPIError 将底层函数返回的初始错误进行一层包装,根据错误级别,包装成不同的错误码进行返回。

其中 ResponseOKResponseError 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
funcResponseOK(c *gin.Context, spec interface{}) {
if spec == nil {
		c.Status(http.StatusNoContent)
return
	}
	c.JSON(http.StatusOK, spec)
}
funcResponseError(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 打印错误日志,输出堆栈
funclog(err error) {
	fmt.Println("========== log start ==========")
	fmt.Printf("%+v\n", err)
	fmt.Println("========== log end ==========")
}
// fakeSendErrorEmail 模拟将错误信息发送到邮件,JSON 格式
funcfakeSendErrorEmail(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 状态码进行了判断,大于 500 的错误将会发送邮件通知,这里使用 fmt.Printf("%#v\n", err) 进行模拟。

其中 apierr.ParseCoder(err) 能够从一个错误对象中获取到实现了 APICoder 的错误码对象,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
funcParseCoder(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 来发送一个请求:curl http://localhost:8080/accounts/1

客户端得到如下响应结果:

1
2
3
4
{
"id": 1,
"name": "account_1"
}

服务端打印正常的请求日志:

Server Log

再来测试下请求一个不存在的账号:curl http://localhost:8080/accounts/12

客户端得到如下响应结果:

1
2
3
4
{
"code": 40401001,
"message": "资源未找到"
}

返回结果中没有 reference 字段,是因为对于 reference 为空的情况,在 JSON 序列化过程中会被隐藏。

服务端打印的错误日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
========== 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 ==========

可以发现,错误日志中不仅打印了错误码([40401001])和错误信息(资源未找到),还打印了错误原因(account not found)以及下面的错误堆栈。

如此清晰的错误日志得益于我们实现的 Format 函数的强大功能。

现在再来触发一个 HTTP 状态码为 500 的错误响应:curl http://localhost:8080/accounts/500

客户端得到如下响应结果:

1
2
3
4
5
{
"code": 50001001,
"message": "系统错误",
"reference": "https://github.com/jianghushinian/gokit/tree/main/errors"
}

这次得到一个带有 reference 字段的完整错误响应。

服务端打印的错误日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
========== log start ==========
[50001001] - 系统错误 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":"系统错误","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 函数打印的日志,还能看到 fakeSendErrorEmail 函数打印的日志,正是一个 JSON 格式的结构化日志。

以上便是我们设计的错误码及错误包在实际开发场景中的应用。

使用建议

根据我的经验,总结了一些错误码及错误包的使用建议,现在将其分享给你。

使用尽量少的 HTTP 状态码

HTTP 状态码大概分为 5 大类,分别是 1XX、2XX、3XX、4XX、5XX。根据我的实际工作经验,我们并不会使用全部的状态码,最常用的状态码不超过 10 个。

所以即使我们设计的业务错误码支持携带 HTTP 状态码,但也不推荐使用过多的 HTTP 状态码,以免加重前端工作量。

推荐在错误码中使用的 HTTP 状态码如下:

  • 400: 请求不合法
  • 401: 认证失败
  • 403: 授权失败
  • 404: 资源未找到
  • 500: 系统错误

其中 4XX 代表客户端错误,而如果是服务端错误,则统一使用 500 状态码,具体错误原因可以通过业务错误码定位。

使用中间件来记录错误日志

由于我们设计的错误包支持 Unwrap 操作,所以建议出现错误时的处理流程如下:

  1. 最底层代码遇到错误时通过 errors.New/fmt.Errorf 来创建一个错误对象,然后将错误返回(可选择性的记录一条日志)。
1
2
3
4
funcQuery(id int)(obj, error) {
// do something
returnnil, fmt.Errorf("%d not found", id)
}
  1. 中间过程中处理函数遇到下层函数返回的错误,不做任何额外处理,直接将其向上层返回。
1
2
3
if err != nil {
return err
}
  1. 在处理用户请求的 Handler 函数中(如 ShowAccount)通过 apierr.WrapC 将错误包装成一个 APIError 返回。
1
2
3
if err != nil {
return apierr.WrapC(CodeNotFound, err)
}
  1. 最上层代码通过在框架层面实现的中间件(如实现一个 after hook middleware)来统一处理错误,打印完整错误日志、发送邮件提醒等,并将安全的错误信息返回给前端。如我们实现的 ResponseError 函数功能。

总结

本篇文章讲解了如何设计一个规范的错误码以及与之配套的错误包。

我参考了一些开源的 API 错误码设计方案,并结合我自己的实际工作经验,给出了我认为比较合理的错误码设计方案。

同时也针对这个错误码方案,设计了一个配套的错误包,来简化使用过程,并给出了我的一些使用建议。

错误包中记录错误堆栈部分的代码参考了 pkg/errors 包实现,感兴趣的同学可以点击进去进行进一步学习。

本文完整代码实现我放在了 Github 上,供你参考使用。

希望本文对你有所启发,如果你有更好的错误码设计方案,欢迎一起交流讨论。

参考

阿里云 ECS 错误码:https://next.api.aliyun.com/document/Ecs/2014-05-26/errorCode

腾讯云服务器错误码:https://cloud.tencent.com/document/api/213/30435

新浪错误码:https://open.weibo.com/wiki/Error_code

pkg/errors:https://github.com/pkg/errors

错误包实现:https://github.com/jianghushinian/gokit/tree/main/errors

相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
7天玩转云服务器
云服务器ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,可降低 IT 成本,提升运维效率。本课程手把手带你了解ECS、掌握基本操作、动手实操快照管理、镜像管理等。了解产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3天前
|
XML JSON API
深入浅出:RESTful API 设计实践与最佳应用
【9月更文挑战第32天】 在数字化时代的浪潮中,RESTful API已成为现代Web服务通信的黄金标准。本文将带您一探究竟,了解如何高效地设计和维护一个清晰、灵活且易于扩展的RESTful API。我们将从基础概念出发,逐步深入到设计原则和最佳实践,最终通过具体案例来展示如何将理论应用于实际开发中。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的指导和灵感。
|
2天前
|
API 开发者 UED
构建高效RESTful API的最佳实践
【9月更文挑战第33天】在数字化时代,后端开发不仅仅是关于代码的编写。它是一场架构艺术的演绎,是性能与可维护性之间的舞蹈。本文将带你深入理解RESTful API设计的精髓,探索如何通过最佳实践提升API的效率和可用性,最终实现后端服务的优雅蜕变。我们将从基础原则出发,逐步揭示高效API设计背后的哲学,并以实际代码示例为路标,指引你走向更优的后端开发之路。
|
8天前
|
JSON Go API
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
使用Go语言和Gin框架构建RESTful API:GET与POST请求示例
|
9天前
|
缓存 监控 测试技术
深入理解RESTful API设计原则与最佳实践
【9月更文挑战第26天】在数字化时代,API(应用程序编程接口)已成为连接不同软件和服务的桥梁。本文将深入浅出地介绍RESTful API的设计哲学、六大约束条件以及如何将这些原则应用到实际开发中,以实现高效、可维护和易于扩展的后端服务。通过具体实例,我们将探索如何避免常见设计陷阱,确保API设计的优雅与实用性并存。无论你是API设计的新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的指导和启示。
|
12天前
|
存储 JSON API
实战派教程!Python Web开发中RESTful API的设计哲学与实现技巧,一网打尽!
在数字化时代,Web API成为连接前后端及构建复杂应用的关键。RESTful API因简洁直观而广受欢迎。本文通过实战案例,介绍Python Web开发中的RESTful API设计哲学与技巧,包括使用Flask框架构建一个图书管理系统的API,涵盖资源定义、请求响应设计及实现示例。通过准确使用HTTP状态码、版本控制、错误处理及文档化等技巧,帮助你深入理解RESTful API的设计与实现。希望本文能助力你的API设计之旅。
37 3
|
12天前
|
存储 前端开发 API
告别繁琐,拥抱简洁!Python RESTful API 设计实战,让 API 调用如丝般顺滑!
在 Web 开发的旅程中,设计一个高效、简洁且易于使用的 RESTful API 是至关重要的。今天,我想和大家分享一次我在 Python 中进行 RESTful API 设计的实战经历,希望能给大家带来一些启发。
27 3
|
11天前
|
缓存 前端开发 API
深入浅出:RESTful API设计的最佳实践
【9月更文挑战第24天】在数字化浪潮中,API作为连接不同软件组件的桥梁,其设计质量直接影响到系统的可维护性、扩展性及用户体验。本文将通过浅显易懂的语言,结合生动的比喻和实例,带领读者深入理解RESTful API设计的核心原则与最佳实践,旨在帮助开发者构建更加健壮、灵活且用户友好的后端服务。
|
11天前
|
开发框架 JSON 缓存
震撼发布!Python Web开发框架下的RESTful API设计全攻略,让数据交互更自由!
在数字化浪潮推动下,RESTful API成为Web开发中不可或缺的部分。本文详细介绍了在Python环境下如何设计并实现高效、可扩展的RESTful API,涵盖框架选择、资源定义、HTTP方法应用及响应格式设计等内容,并提供了基于Flask的示例代码。此外,还讨论了版本控制、文档化、安全性和性能优化等最佳实践,帮助开发者实现更流畅的数据交互体验。
32 1
|
1月前
|
JSON 算法 安全
探索RESTful API设计的最佳实践
【9月更文挑战第2天】在数字化时代的浪潮中,后端开发如同搭建一座桥梁,连接着用户与数据的无限可能。本文将深入探讨如何打造高效、可维护的RESTful API,从资源命名到状态码的巧妙运用,每一个细节都隐藏着提升用户体验的智慧。你将学会如何在浩瀚的代码海洋中,用简洁明了的设计原则,引领用户安全抵达数据的彼岸。让我们一起启航,探索API设计的奥秘,让后端开发成为艺术与科学的完美结合。
|
21天前
|
JSON 前端开发 API
打造高效后端:RESTful API 设计的最佳实践
【9月更文挑战第14天】在数字化时代,后端开发是构建强大、灵活和可维护应用程序的基石。本文将深入探讨如何设计高效的RESTful API,包括清晰的资源定义、合理的HTTP方法使用、URL结构规划、状态码的准确返回以及数据格式的设计。通过这些实践,开发者能够创建出既符合行业标准又易于维护和扩展的API,为前端提供强大的数据支持,确保整个应用的稳定性和性能。
157 74