从 Flask 到 Gin —— 处理 JSON

文章目录

本文是 从 Flask 到 Gin 系列的第 1 篇。


如果写过 C++ 或者 Java,你会觉得在 Golang 中处理 JSON 比前两者要简单很多。但作为习惯了在 Python 中偷懒的我来说,Golang 中的 JSON 用法还是挺难受的。

MJP 是一个 RESTful API 服务,绝大多数 API 返回的都是 JSON 格式。由于 Python 的灵活性和 Flask 的良好封装,编写 MJP 服务时,我在 Flask 中使用 JSON 没有遇到什么困难。MJP 中有一个名为 responseto 的封装,我使用它来统一输出 JSON Response。本文介绍将这个方法移植到 Gin 的过程。

responseto 的 Python 实现

responseto 是一个 Python 中的模块方法,它的定义如下:

 1from flask import jsonify
 2from mjp.app import db
 3
 4def responseto(message=None, error=None, code=None, data=None, replaceobj=None, replaceobj_key_only=False, **kwargs):
 5    """
 6    封装 json 响应
 7    :param message: 错误消息,若提供则默认 error 为 True
 8    :param error: 是否包含错误
 9    :param code: 错误代码,若不提供则值可能为 200 error=False/444 error=True
10    :param data: 若提供了 data,则 data 中应该包含 error/message/code
11    :param replaceobj: 替换响应中的键名。 {'被替换': '替换值'}
12    :param kwargs: 要加入响应的其他对象,可以是 model 也可以是 dict
13    :return: 一个 Response 对象
14    """
15
16    # 如果提供了 data,那么不理任何其他参数,直接响应 data
17    if not data:
18        data = kwargs
19        for k, v in kwargs.items():
20            # 不处理空对象
21            if not v:
22                continue
23            data[k] = db.to_response_data(v, replaceobj, replaceobj_key_only)
24        data['error'] = error
25        data['code'] = code
26        if message:
27            # 除非显示提供 error 的值,否则默认为 True
28            # 意思是提供了 message 就代表有 error
29            data['message'] = message
30            if error is None:
31                data['error'] = True
32            if not data.get('code'):
33                data['code'] = 444
34        else:
35            # 除非显示提供 error 的值,否则默认为 False
36            # 意思是没有提供 message 就代表没有 error
37            if error is None:
38                data['error'] = False
39            if not data.get('code'):
40                data['code'] = 200
41    if not isinstance(data, dict):
42        # 444 为不合法操作
43        data = {'error': True, 'code': 444, 'message': 'data 必须是一个 dict!'}
44    if not data.get('code'):
45        if data.get('error'):
46            data['code'] = 444
47        else:
48            data['code'] = 200
49    resp = jsonify(data)
50    return resp

上面的源码注释很清晰,不用多言。jsonify 是 Flask 提供的一个 JSON Response 封装,它会返回一个 JSON 响应。

有必要多说一句的是 db.to_response_data。这个方法把 SQLAlchemy 的 Model 对象(一般是数据库查询的结果)转换成为一个 dict,接着使用 jsonify 将其转换成为 JSON Response。因为与本文关系不大,这里就不贴 db.to_response_data 的源码了。

responseto 方法在路由中使用。下面是一个获取奖品信息的路由方法示例:

 1@reward.route('/get/', methods=['GET'])
 2@login_token_checker()
 3def reward_status_get(r, loginobj):
 4    """ 获取邀请码对应的奖励信息
 5    :param invitecode: 奖励邀请码
 6    """
 7    invitecode = parse_int(request.args.get('invitecode'))
 8    if invitecode is None:
 9        return responseto('需要 invitecode!', code=401)
10
11    # 保存奖励的 Table
12    Reward = get_reward_table(r)
13    reward_result = Reward.query.filter(Reward.invitecode == invitecode).all()
14    if reward_result is None:
15        reward_result = []
16    else:
17        rewards = []
18        # item 是 Reward Table 中的一行数据
19        for item in reward_result:
20            # rvalue 是一个保存在数据库中的 JSON 字符串
21            result = json.loads(item.rvalue)
22            # 当前奖励行的状态
23            result['status'] = item.status
24            rewards.append(result)
25        reward_result = rewards
26
27    return responseto(result=reward_result)

在上面的路由方法中,当没有提供 invitecode 导致请求失败的时候,responseto 返回的内容为:

1{
2    "error": true,
3    "code": 401,
4    "message": "需要 invitecode!"
5}

当请求成功的时候,JSON 内容可能为:

 1{
 2    "error": false,
 3    "code": 200,
 4    "result": [
 5        {
 6            "gold": 10,
 7            "status": 1
 8        },
 9        {
10            "money": 3000,
11            "status": 2
12        }
13    ]
14}

从上面的例子可以看出,得益于 Pyhon 方法的 kwargs 参数机制,responseto 的使用可以非常灵活。使用中既可以省略许多参数,也可以动态调整返回的 JSON 内容的键名。

这些灵活的用法,在 Golang 中会遇到挑战。

responseto 在 Golang 中的挑战

Golang/gin 中实现 responseto 方法,至少会碰到 3 个问题。

  1. JSON 对应的 Struct 问题。
  2. Golang 不支持关键字参数的问题。
  3. 动态 JSON 结构的问题。

让我们来解决这些问题。

gin.H 和 Context.JSON

查看 gin 的源码,可以找到一些良好的封装。gin.H(utils.go) 提供了一个类似于 Python dict 的结构。Context.JSON(context.go) 提供了类似于 Flask jsonify 的方法。

 1// utils.go
 2// H is a shortcut for map[string]interface{}
 3type H map[string]interface{}
 4
 5// context.go
 6// JSON serializes the given struct as JSON into the response body.
 7// It also sets the Content-Type as "application/json".
 8func (c *Context) JSON(code int, obj interface{}) {
 9	c.Render(code, render.JSON{Data: obj})
10}

Responseto/ResponsetoWithData

Golang 不提供方法重载和关键字参数,因此我创建了 3 个方法来替代 Flask 版本的 responseto

 1package re2q
 2
 3import (
 4	"net/http"
 5
 6	"github.com/gin-gonic/gin"
 7)
 8
 9// Responseto 使用 gin.Context 创建一个响应,提供 message/code
10func Responseto(c *gin.Context, message string, code int) {
11	ResponsetoWithAllData(c, gin.H{
12		"error":   code != 200,
13		"code":    code,
14		"message": message,
15	})
16}
17
18// ResponsetoWithData 使用 gin.Context 创建一个响应,自动填充 error/code/message
19func ResponsetoWithData(c *gin.Context, data gin.H) {
20	message := data["message"]
21	if message != nil {
22		data["message"] = message
23	}
24	code, ok := data["code"]
25	if !ok {
26		if message == nil {
27			code = 200
28		} else {
29			code = 444
30		}
31	}
32	if err, ok := data["error"]; ok {
33		data["error"] = err
34	} else {
35		data["error"] = code != 200
36	}
37	data["code"] = code
38	ResponsetoWithAllData(c, data)
39}
40
41// ResponsetoWithAllData 使用 gin.Context 创建一个响应,直接使用包含了 message/code/error 的 data
42func ResponsetoWithAllData(c *gin.Context, data gin.H) {
43	c.JSON(http.StatusOK, data)
44}

上面的三个方法层层递进,提供了 Flask 版本 responseto 能提供的 大部分 功能。想完整实现 Flask 版本 responseto 的功能,需要定义更多的方法,或者采用 Golang 中的变长参数。这样会让方法变得更加负责,让方法的使用者产生困扰。

在跨语言移植功能的时候,”保持绝对完全一致“ 是没有必要的。每个语言都有自己独特的特性,我们作为架构设计者,需要在不同语言中进行折衷。我认为上面 3 个方法既保证了简洁的抽象,也保持了一定的使用灵活性。这是一个合理的移植决策。

下面是 ResponsetoResponsetoWithData 在路由中使用的例子:

 1// AudibleActive return active users list
 2func AudibleActive(c *gin.Context) {
 3	regionals, defaultDBQry := registerOrActive(c)
 4	if regionals != nil {
 5		// 查找活跃数据
 6		actives := []models.ActiveModel{}
 7		findError := defaultDBQry.Find(&actives).Error
 8		if findError != nil {
 9			re2q.Responseto(c, findError.Error(), 503)
10			return
11		}
12		activesSerializer := models.ActivesSerializer{c, actives}
13		re2q.ResponsetoWithData(c, gin.H{
14			"registers": activesSerializer.Response(),
15			"regionals": regionals,
16			"code":      200,
17		})
18	}
19}

和上面 Flask 路由的例子类似,第一个 Responseto 方法提供了一个包含 error/code/message 键名的 JSON 对象,第二个 ResponsetoWithData 方法提供了一个带有具体数据返回的 JSON 对象。

看吧,完美!

序列化

然鹅并没有那么完美。

也许你注意到了上面代码中的 models.ActiveSerializer 这个名称,是的,序列化必须自己动手。

由于 Golang 的语言特色(Public 必须大写字母开头),在定义数据库字段的时候,你必须要进行一些映射。另外,你也不一定希望所有的数据库字段都返回给客户端,因此需要手动序列化。

 1package models
 2
 3import (
 4	"github.com/gin-gonic/gin"
 5)
 6
 7// ActiveModel is a table for active users
 8type ActiveModel struct {
 9	Gid     int `gorm:"PRIMARY_KEY,AUTO_INCREMENT"`
10	Date    int `gorm:"PRIMARY_KEY,INDEX"`
11	Channel int `gorm:"PRIMARY_KEY,INDEX"`
12	Num     int `gorm:"NOT NULL"`
13}
14
15// ActiveResponse is a JSON config for response
16type ActiveResponse struct {
17	Gid     int `json:"gid"`
18	Date    int `json:"date"`
19	Channel int `json:"channel"`
20	Num     int `json:"num"`
21}
22
23// ActiveSerializer is a secializer for JSON object
24type ActiveSerializer struct {
25	C *gin.Context
26	ActiveModel
27}
28
29// ActivesSerializer is a secializer for JSON list
30type ActivesSerializer struct {
31	C       *gin.Context
32	Actives []ActiveModel
33}
34
35// Response is for JSON response
36func (s *ActiveSerializer) Response() ActiveResponse {
37	response := ActiveResponse{
38		Gid:     s.Gid,
39		Date:    s.Date,
40		Channel: s.Channel,
41		Num:     s.Num,
42	}
43	return response
44}
45
46// Response is for JSON response
47func (s *ActivesSerializer) Response() []ActiveResponse {
48	response := []ActiveResponse{}
49	for _, active := range s.Actives {
50		serializer := ActiveSerializer{s.C, active}
51		response = append(response, serializer.Response())
52	}
53	return response
54}

在上面的代码中,ActiveModel 是一个数据表定义。ActiveResponse 用来完成数据库字段与 JSON 响应之间的映射。

要了解 ActiveModelgorm,请参考: Flask 到 Gin —— SQLAlchemy 和 gorm

需要注意 ActiveSerializerActivesSerializer 的区别(后者是复数形式)。它们分别用来返回 一个 ActiveModel 对象和 一组 ActiveModel 对象。在 Python 这类动态语言中,我们可以将它们放在同一个方法中,在 Golang 中则必须分开处理。

如果不希望这么麻烦,只想简单把 JSON 转换成 Struct 定义,可以使用这个网站: JSON-to-Go

Go by Example: JSON 详细介绍了在 Golang 中使用 JSON 的一些特性,适合初学者阅读。


阅读系列所有文章:从 Flask 到 Gin

全文完