「Go」Go-搭建IM即时通讯系列文章2-统一返回值及异常处理

在软件开发过程中,通过定义统一返回值还可以提高框架的可用性和可维护性,让开发人员更容易理解和使用框架中的各种功能。同时统一异常处理能够帮助我们更好地控制程序的流程,能够让我们更好地捕获异常并作出相应的处理。这样做可以帮助我们减少代码重复,提高代码的可读性和可维护性。

统一返回值

我们规范约定返回值参数有利于我们对数据进行管理以及提升前后端开发的效率。

项目根目录/src/main/ 目录下新建一个common/app 层级目录,并在 app目录下分别建立 code.gomsg.goresponse.go 用于存放 返回值、返回消息、统一返回值的实体对象。

code.go 主要定义返回值常量,代码如下:

1
2
3
4
5
6
7
8
9
10
package app

import "net/http"

const (
// 成功使用 200
SUCCESS = http.StatusOK
// 异常使用 -1
ERROR = -1
)

msg.go 主要定义返回值常量对应的消息内容,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package app

// MessageMap 返回值常量对应的消息内容,消息集合:{消息码,消息内容}
var MessageMap = map[int]string{
SUCCESS: "成功",
ERROR: "失败",
}

// GetMsg 根据代码获取返回信息
func GetMsg(code int) string {
msg, ok := MessageMap[code]
if ok {
return msg
}

return MessageMap[ERROR]
}

response.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
package app

import (
"github.com/gin-gonic/gin"
"net/http"
)

// Response 响应对象
type Response struct {
// 响应编码
Code int `json:"code"`
// 返回消息
Msg string `json:"msg"`
// 返回数据
Data interface{} `json:"data"`
}

// Res 设置 gin.JSON 的内容
func Res(c *gin.Context, httpCode, errCode int, data interface{}) {
c.JSON(httpCode, Response{
Code: errCode,
Msg: GetMsg(errCode),
Data: data,
})
return
}

// Success 返回成功结果
func Success(c *gin.Context, data interface{}) {
Res(c, http.StatusOK, SUCCESS, data)
}

// Error 返回错误结果,异常结果放在统一异常处理 handler中
func Error(c *gin.Context, data interface{}) {
Res(c, http.StatusOK, ERROR, data)
}

这时我们可以修改 src/main/routers/api/health.go 中返回的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package api

import (
"github.com/gin-gonic/gin"
"star-im/src/main/common/app"
)

// Ping 接口连通性测试
func Ping(c *gin.Context) {
// 直接返回成功结果
app.Success(c, nil)
}

这时候我们通过浏览器访问 http://localhost:8081/ping,得到如下返回:

1
2
3
4
5
{
"code":200,
"msg":"成功",
"data":null,
}

这时候我们就可以根据不同的返回值进行不同的业务处理了

统一异常处理

我们需要统一处理系统的异常信息并让异常结果也显示为统一的返回结果对象,那么需要进行统一异常处理。

项目根目录/src/main/ 目录下新建一个 handler 目录,并在目录下新建一个 exception.go 文件,用于处理异常信息

exception.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
package handler

import (
"github.com/gin-gonic/gin"
"log"
"net/http"
"runtime/debug"
"star-im/src/main/common/app"
)

// Recover 注意 Recover 要尽量放在router.User的第一个被加载
// 如不是的话,在recover前的中间件或路由,将不能被拦截到
// 程序的原理是:
// 1.请求进来,执行recover
// 2.程序异常,抛出panic
// 3.panic被 recover捕获,返回异常信息,并Abort,终止这次请求
func Recover(c *gin.Context) {
defer func() {
r := recover()
if r != nil {
//打印错误堆栈信息
log.Printf("panic: %v\n", r)
debug.PrintStack()
//封装通用json返回
c.JSON(http.StatusOK, app.Response{
Code: app.ERROR,
Msg: ErrorToString(r),
Data: nil,
})
//终止后续接口调用,不加的话recover到异常后,还会继续执行接口里后续代码
c.Abort()
}
}()

//加载完 defer recover,继续后续接口调用
c.Next()
}

// ErrorToString recover错误,转string
func ErrorToString(r interface{}) string {
switch v := r.(type) {
case error:
return v.Error()
default:
return r.(string)
}
}

该类主要捕获panic异常,并返回 json 信息给客户端

src/main/routers/router.go 文件中添加如下代码即可。

1
2
// 统一异常处理
r.Use(handler.Recover)

此时我们可以修改router/ping 的方法来测试结果

src/main/routers/router.go 中如下代码

1
r.GET("/ping", api.Ping)

修改为:

1
2
3
4
5
r.GET("/ping", func(c *gin.Context) {
// 无意抛出 panic
var slice = []int{1, 2, 3, 4, 5}
slice[6] = 6
})

然后重启项目,通过浏览器访问 http://localhost:8081/ping,得到如下返回:

1
2
3
4
5
{
"code":-1,
"msg":"runtime error: index out of range [6] with length 5",
"data":null,
}

现在我们得到的就是统一的异常返回值,这里的 msg 可以根据需要再进行修改。

在业务中我们可以通过判断业务逻辑,再进行抛出异常,如将刚才函数中的内容修改为如下代码:

1
2
3
4
5
6
r.GET("/ping", func(c *gin.Context) {
// 抛出 panic, true这里可以改为自己的业务处理逻辑
if true {
panic("抛出了指定的异常信息")
}
})

通过浏览器访问 http://localhost:8081/ping,得到如下返回:

1
2
3
4
5
{
"code":-1,
"msg":"抛出了指定的异常信息",
"data":null,
}

可以看到成功捕获了 panic 抛出的异常信息,这可以很方便我们做业务逻辑的时候处理异常。(测试成功后记得把ping的函数恢复回之前的)

gin router 中也提供了针对 NoRoute 和 NoMethod 的处理,在 router.go 中添加

1
2
3
// 处理404
r.NoRoute(handler.HandleNotFound)
r.NoMethod(handler.HandleNotFound)

src/main/handler/exception.go 中添加处理方法,直接返回处理结果即可

1
2
3
4
5
6
7
8
9
10
11
func HandleNotFound(c *gin.Context) {
// 我这里使用的是统一返回值
app.ErrorWithCode(c, app.NOT_FOUND, nil)
// // 你们也可以直接使用普通的返回结果,如下
// c.JSON(http.StatusOK, gin.H{
// "msg": "找不到资源",
// "code": 404,
// "data": ""
// })
return
}

swag 接口文档

Swagger是一种API框架,它可以为REST APIs定义、生成、测试和文档化。它使用一种名为Swagger元数据的特殊格式来描述API,并使用Swagger UI来展示API的定义。这使得开发人员可以在不离开API文档的情况下测试API,并且可以轻松地为API创建文档。

我们引入 Swagger 来生成接口文档,方便统一管理接口及调试。

相关链接:

GitHub

安装

1
go install github.com/swaggo/swag/cmd/swag@latest

使用

1
swag init -o "src/main/docs"
  • -o 为 output,指定输出目录,默认为“./docs”

其他更多文章参考 GitHub中文文档

在主程序入口 main.go 中可以添加如下注释:

1
2
3
4
5
6
// @title           Star-IM
// @version 1.0
// @description 即时通讯接口文档
func main() {
……
}

src/main/routers/api/health.go 文件中添加如下注释

1
2
3
4
5
6
7
8
9
// Ping
// @Summary 健康检查
// @Description 接口连通性测试
// @Tags 测试
// @Success 200 {object} app.Response
// @Router /ping [get]
func Ping(c *gin.Context) {
……
}

src/main/routers/router.go 加入swagger接口文档的访问,并引入指定了目录的swagger文件

代码如下:

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

import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"io"
"os"
_ "star-im/src/main/docs"
"star-im/src/main/handler"
"star-im/src/main/routers/api"
)

// Setup 初始化路由
func Setup() *gin.Engine {
r := gin.Default()
// 记录到文件
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)

// 使用中间件
// 统一异常处理
r.Use(handler.Recover)
// 统一日志
r.Use(gin.Logger())

// 不需要鉴权
r.GET("/ping", api.Ping)

r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}

主要变更内容为

  • import _ "star-im/src/main/docs"
  • import swaggerFiles "github.com/swaggo/files"
  • import ginSwagger "github.com/swaggo/gin-swagger"
  • r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

测试

通过浏览器访问 http://localhost:8081/swagger/index.html#/ 可以进入到swagger 接口文档的管理界面

至此,当前目录结构为:

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
.
├── LICENSE
├── README.md
├── doc
│   └── build
│   └── 1-framwork.md
├── gin.log
├── go.mod
├── go.sum
├── main.go
└── src
├── main
│   ├── common
│   │   └── app
│   │   ├── code.go
│   │   ├── msg.go
│   │   └── response.go
│   ├── config
│   │   ├── database
│   │   │   └── database.go
│   │   ├── init.go
│   │   └── settings
│   │   └── settings.go
│   ├── docs
│   │   ├── docs.go
│   │   ├── swagger.json
│   │   └── swagger.yaml
│   ├── handler
│   │   └── exception.go
│   ├── models
│   │   └── model.go
│   ├── routers
│   │   ├── api
│   │   │   ├── health.go
│   │   │   └── v1
│   │   └── router.go
│   └── util
├── resource
│   └── app.yml
└── test
└── pkg
├── test_gin.go
├── test_gorm.go
├── test_jwt.go
├── test_redis.go
└── test_viper.go