从 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 个问题。
- JSON 对应的 Struct 问题。
- Golang 不支持关键字参数的问题。
- 动态 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 个方法既保证了简洁的抽象,也保持了一定的使用灵活性。这是一个合理的移植决策。
下面是 Responseto
和 ResponsetoWithData
在路由中使用的例子:
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 响应之间的映射。
要了解 ActiveModel
中 gorm
,请参考: Flask 到 Gin —— SQLAlchemy 和 gorm。
需要注意 ActiveSerializer
和 ActivesSerializer
的区别(后者是复数形式)。它们分别用来返回 一个 ActiveModel
对象和 一组 ActiveModel
对象。在 Python 这类动态语言中,我们可以将它们放在同一个方法中,在 Golang 中则必须分开处理。
如果不希望这么麻烦,只想简单把 JSON 转换成 Struct 定义,可以使用这个网站: JSON-to-Go。
Go by Example: JSON 详细介绍了在 Golang 中使用 JSON 的一些特性,适合初学者阅读。
阅读系列所有文章:从 Flask 到 Gin。
- 文章ID:2682
- 原文作者:zrong
- 原文链接:https://blog.zengrong.net/post/flask-to-gin-json/
- 版权声明:本作品采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可,非商业转载请注明出处(原文作者,原文链接),商业转载请联系作者获得授权。