引言


这篇博客不是关于错误堆栈的实现的,反正我的项目太小用不到(NX的嘲讽)

在Gin框架中,我们往往会直接使用下面形式进行错误处理

1
2
3
4
5
6
if err!=nil{
c.JSON(http.statusOK,gin.H{
"msg" : "ok",
"error" : err
})
}

然而,我们会发现这种错误返回的方式,没有固定的结构,并且书写的时候非常冗余

那么我们就需要有一种固定形式来返回错误,同时丰富错误

算了说白了就是把c.JSON封装一下,更加高级一点,下面看一下实现

实现


结构

1
2
3
4
errs:
- code.go
- logic.go
- response.go

code.go:

预定义字段

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
package errs
// 这个代码是用作特定类型定义
//可以预定义一些成功类型和错误类型,以便在整个系统中使用
//参照NX的博客,反正就是参照了HTTP状态码的语义,方便识别错误类型
// 这里的code都是*Error的类型
var (
SUCCESS = newError(200, "Success")
)

var (
INVALID_REQUEST = newError(40001, "无效的请求")
NOTFOUND = newError(40002, "目标不存在")
HAS_EXIST = newError(40003, "目标已存在")
LOGIN_ERROR = newError(40004, "d登陆失败")
UNTHORIZATION = newError(40005, "鉴权失败")
)

var (
DB_LINK_ERROR = newError(50001, "连接数据库失败")
DB_CRUD_ERROR = newError(50002, "数据库操作失败")
DB_BASE_ERROR = newError(50003, "数据库内部错误")
)

var (
SERVE_INTERNAL = newError(60001, "服务器内部故障")
)

logic.go:

整体代码

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
package errs

import (
"errors"
"fmt"
)

// 下面展现如何生成“错误”的过程

type Error struct {
Code int64 `json:"code"`
Message string `json:"message"`
Origin string `json:"origin"` // Origin字段通常用于只是错误的来源或者上下文
}

// 绑定数字和文字之间的关联
func newError(code int64, msg string) *Error {
return &Error{
Code: code,
Message: msg,
}
}

// 表示数字背后的含义(人能看懂)
func (e *Error) Error() string {
return e.Message
}

func (e *Error) Is(target error) bool {
var t *Error
// errors.As是把target转化成t的类型
ok := errors.As(target, &t)
if !ok {
return false
}
return e.Code == t.Code
}

// 添加error类型
func (e *Error) WithOrigin(err error) *Error {
return &Error{
Code: e.Code,
Message: e.Message,
Origin: fmt.Sprintf("%+v", err),
}
}

// 添加字符串进入
func (e *Error) WithTips(details ...string) *Error {
return &Error{
Code: e.Code,
Message: e.Message + " " + fmt.Sprintf("%v", details),
}
}

解析

  1. 返回结构

    1
    2
    3
    4
    5
    type Error struct {
    Code int64 `json:"code"`
    Message string `json:"message"`
    Origin string `json:"origin"` // Origin字段通常用于只是错误的来源或者上下文
    }
  2. 绑定预定义字段的基础含义

    1
    2
    3
    4
    5
    6
    7
    // 绑定数字和文字之间的关联
    func newError(code int64, msg string) *Error {
    return &Error{
    Code: code,
    Message: msg,
    }
    }
  3. 默认error与自定义Error类型转换 —- errors.As

    1
    2
    3
    4
    5
    var t *Error
    ok := errors.As ( _ error, &t)
    if !ok{
    ....
    }
  4. 丰富Error字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 添加error类型
    func (e *Error) WithOrigin(err error) *Error {
    return &Error{
    Code: e.Code,
    Message: e.Message,
    Origin: fmt.Sprintf("%+v", err),
    }
    }

    // 添加字符串进入
    func (e *Error) WithTips(details ...string) *Error {
    return &Error{
    Code: e.Code,
    Message: e.Message + " " + fmt.Sprintf("%v", details),
    }
    }

    两种方法都能够使用,一种是添加error类型,一种是添加字符串

    注意这两个是方法,需要与预定义字段相结合来使用。

  5. 输出错误字符串

    1
    2
    3
    func (e *Error) Error() string {
    return e.Message
    }

    类似于 error.Error()就是来输出错误字符串的

response.go:

Success() + Fail()

定义一个响应的结构体字段

1
2
3
4
5
6
type responseBody struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Origin string `json:"origin"`
Data any `json:"data"`
}

前面三个字段是来对应之前Error的基础字段的

后面一个Data字段就是用来丰富Error数据内容的

Success:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...any可以当作切片来处理
func Success(c *gin.Context, data ...any) {
response := responseBody{
Code: SUCCESS.Code,
Msg: SUCCESS.Message,
Origin: SUCCESS.Origin,
Data: data,
}
//返回切片的第一个数据,但是我觉得全部返回就行,所以就去掉了
//if len(data) > 0 {
// response.Data = data[0]
//}
c.JSON(http.StatusOK, response)
}

因为传入 data …data 意味着可以无限传入不同数据类型的数据,因此丰富性提高

Fail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Fail(c *gin.Context, err error) {
var e *Error
ok := errors.As(err, &e)
if !ok {
e = SERVE_INTERNAL.WithOrigin(err)
}

var resp responseBody
resp.Code = e.Code
resp.Msg = e.Message
resp.Origin = e.Origin

c.JSON(int(e.Code/100), resp)
c.Abort()
}

注意 e = SERVE_INTERNAL.WithOrigin(err) 这个用法

SERVE_INTERNAL 是Error类型的 WithOrigin是Error类型的方法

因此用这个方法来引出是哪方面的基础错误-“服务器故障”

同时还可以附加自己的错误 - “err”

运行


随便挑一端代码

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
// 用户的登录
func Login(c *gin.Context) {
// 用PostForm 传输数据
name := c.PostForm("name")
password := c.PostForm("password")

var v2 model.User
if tx := db.DB.Where(" name = ? ", name).First(&v2); tx.Error != nil {
log.SugarLogger.Error(tx.Error)
errs.Fail(c, errs.DB_CRUD_ERROR.WithOrigin(tx.Error))
return
}

if password != v2.Password {
errs.Fail(c, errs.LOGIN_ERROR.WithTips("密码错误"))
return
}

token, err := jwt.NewToken(name)
if err != nil {
log.SugarLogger.Error(err)
errs.Fail(c, errs.UNTHORIZATION.WithOrigin(err))
return
}

errs.Success(c, v2, map[string]string{"token": token})
}

成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 200,
"msg": "Success",
"origin": "",
"data": [
{
"ID": 1,
"CreatedAt": "2024-01-27T13:14:54.198+08:00",
"UpdatedAt": "2024-02-02T15:48:39.952+08:00",
"DeletedAt": null,
"name": "John",
"gender": "male",
"age": 25,
"birthday": "1999-01-01T08:00:00+08:00",
"telephone": "1234567890",
"password": "thirdsecreword",
"email": "john@example.com"
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJ1c2VyIjoiSm9obiIsImlzcyI6IkVjaGluIiwic3ViIjoiVG9tIiwiZXhwIjoxNzA2OTY0Njc3fQ.iZLzNf3IUpFMPr1kV54nw8uugAkRzlu0VoJTWvlZjps"
}
]
}

失败:

1
2
3
4
5
6
{
"code": 50002,
"msg": "数据库操作失败",
"origin": "record not found",
"data": null
}

总结

​ 整体的实现就是把c.JSON包装了一下,使得错误都统一化为预定义字段,同时还可以返回一些个人想要返回的个性化数据(通过WithOrigin WithTips)

​ 再次强调,这个跟错误堆栈没啥关系,狗屎NX误导我,前一段时间全在思考为啥这个方法能够实现错误堆栈,明明都无法向上抛,