Go Ginrest实现一个RESTful接口
作者:jiaxwu 发布时间:2024-05-21 10:26:08
背景
基于现在微服务或者服务化的思想,我们大部分的业务逻辑处理函数都是长这样的:
比如grpc服务端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq) (*pb.GetUserInfoRsp, error) {
// 业务逻辑
// ...
}
grpc客户端:
func (s *Service) GetUserInfo(ctx context.Context, req *pb.GetUserInfoReq, opts ...grpc.CallOption) (*pb.GetUserInfoRsp, error) {
// 业务逻辑
// ...
}
有些服务我们需要把它包装为RESTful形式的接口,一般需要经历以下步骤:
指定HTTP方法、URL
鉴权
参数绑定
处理请求
处理响应
可以发现,参数绑定、处理响应几乎都是一样模板代码,鉴权也基本上是模板代码(当然有些鉴权可能比较复杂)。
而Ginrest库就是为了消除这些模板代码,它不是一个复杂的框架,只是一个简单的库,辅助处理这些重复的事情,为了实现这个能力使用了Go1.18的泛型。
仓库地址:github.com/jiaxwu/ginr…
特性
这个库提供以下特性:
封装RESTful请求响应
封装RESTful请求为标准格式服务
封装标准格式服务处理结果为标准RESTful响应格式:Rsp{code, msg, data}
默认使用统一数字错误码格式:[0, 4XXXX, 5XXXX]
默认使用标准错误格式:Error{code, msg}
默认统一状态码[200, 400, 500]
提供Recovery中间件,统一panic时的响应格式
提供SetKey()、GetKey()方法,用于存储请求上下文(泛型)
提供ReqFunc(),用于设置Req(泛型)
使用例子
示例代码在:github.com/jiaxwu/ginr…
首先我们实现两个简单的服务:
const (
ErrCodeUserNotExists = 40100 // 用户不存在
)
type GetUserInfoReq struct {
UID int `json:"uid"`
}
type GetUserInfoRsp struct {
UID int `json:"uid"`
Username string `json:"username"`
Age int `json:"age"`
}
func GetUserInfo(ctx context.Context, req *GetUserInfoReq) (*GetUserInfoRsp, error) {
if req.UID != 10 {
return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
}
return &GetUserInfoRsp{
UID: req.UID,
Username: "user_10",
Age: 10,
}, nil
}
type UpdateUserInfoReq struct {
UID int `json:"uid"`
Username string `json:"username"`
Age int `json:"age"`
}
type UpdateUserInfoRsp struct{}
func UpdateUserInfo(ctx context.Context, req *UpdateUserInfoReq) (*UpdateUserInfoRsp, error) {
if req.UID != 10 {
return nil, ginrest.NewError(ErrCodeUserNotExists, "user not exists")
}
return &UpdateUserInfoRsp{}, nil
}
然后使用Gin+Ginrest包装为RESTful接口:
可以看到Register()里面每个接口都只需要一行代码!
func main() {
e := gin.New()
e.Use(ginrest.Recovery())
Register(e)
if err := e.Run("127.0.0.1:8000"); err != nil {
log.Println(err)
}
}
// 注册请求
func Register(e *gin.Engine) {
// 简单请求,不需要认证
e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo))
// 认证,绑定UID,处理
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = GetUID(c)
} // 这里拆多一步是为了显示第一个参数是ReqFunc
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
}
const (
KeyUserID = "KeyUserID"
)
// 简单包装方便使用
func GetUID(c *gin.Context) int {
return ginrest.GetKey[int](c, KeyUserID)
}
// 简单包装方便使用
func SetUID(c *gin.Context, uid int) {
ginrest.SetKey(c, KeyUserID, uid)
}
// 认证
func Verify(c *gin.Context) {
// 认证处理
// ...
// 忽略认证的具体逻辑
SetUID(c, 10)
}
运行上面代码,然后尝试访问接口,可以看到返回结果:
请求1
GET http://127.0.0.1:8000/user/info/get
{
"uid": 10
}
响应1
{
"code": 0,
"msg": "ok",
"data": {
"uid": 10,
"username": "user_10",
"age": 10
}
}
请求2
GET http://127.0.0.1:8000/user/info/get
{
"uid": 1
}
响应2
{
"code": 40100,
"msg": "user not exists"
}
请求3
POST http://127.0.0.1:8000/user/info/update
{
"username": "jiaxwu",
"age": 10
}
响应3
{
"code": 0,
"msg": "ok",
"data": {}
}
实现原理
Do()和DoOpt()都会转发到do(),它其实是一个模板函数,把脏活累活给处理了:
// 处理请求
func do[Req any, Rsp any, Opt any](reqFunc ReqFunc[Req],
serviceFunc ServiceFunc[Req, Rsp], serviceOptFunc ServiceOptFunc[Req, Rsp, Opt], opts ...Opt) gin.HandlerFunc {
return func(c *gin.Context) {
// 参数绑定
req, err := BindJSON[Req](c)
if err != nil {
return
}
// 进一步处理请求结构体
if reqFunc != nil {
reqFunc(c, req)
}
var rsp *Rsp
// 业务逻辑函数调用
if serviceFunc != nil {
rsp, err = serviceFunc(c, req)
} else if serviceOptFunc != nil {
rsp, err = serviceOptFunc(c, req, opts...)
} else {
panic("must one of ServiceFunc and ServiceFuncOpt")
}
// 处理响应
ProcessRsp(c, rsp, err)
}
}
功能列表
处理请求
用于把一个标准服务封装为一个RESTfulgin.HandlerFunc
,对应Do()、DoOpt()函数。
DoOpt()相比于Do()多了一个opts参数,因为很多rpc框架客户端都有一个opts参数作为结尾。
还有一个BindJSON()
,用于把请求体包装为一个Req结构体:
// 参数绑定
func BindJSON[T any](c *gin.Context) (*T, error) {
var req T
if err := c.ShouldBindJSON(&req); err != nil {
FailureCodeMsg(c, ErrCodeInvalidReq, "invalid param")
return nil, err
}
return &req, nil
}
如果无法使用Do()和DoOpt()则可以使用此方法。
处理响应
用于把rsp、error、errcode、errmsg等数据封装为一个JSON格式响应体,对应ProcessRsp()、Success()、Failure()、FailureCodeMsg()函数。
比如ProcessRsp()
需要带上rsp和error,这样业务里面就不需要再写如下模板代码了:
// 处理简单响应
func ProcessRsp(c *gin.Context, rsp any, err error) {
if err != nil {
Failure(c, err)
return
}
Success(c, rsp)
}
响应格式统一为:
// 响应
type Rsp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data,omitempty"`
}
Success()
用于处理成功情况:
// 请求成功
func Success(c *gin.Context, data any) {
ginRsp(c, http.StatusOK, &Rsp{
Code: ErrCodeOK,
Msg: "ok",
Data: data,
})
}
其余同理。
如果无法使用Do()和DoOpt()则可以使用这些方法。
处理错误
一般我们都需要在出错时带上一个业务错误码,方便客户端处理。因此我们需要提供一个合适的error类型:
// 错误
type Error struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
我们提供了一些函数方便使用Error
,对应NewError()、ToError()、ErrCode()、ErrMsg()、ErrEqual()函数。
比如NewError()
生成一个Error类型error:
// 通过code和msg产生一个错误
func NewError(code int, msg string) error {
return &Error{
Code: code,
Msg: msg,
}
}
请求上下文操作
Gin的请求是链式处理的,也就是多个handler顺序的处理一个请求,比如:
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = ginrest.GetKey[int](c, KeyUserID)
}
// 认证,绑定UID,处理
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
这个接口经历了Verify和ginrest.Do两个handler,其中我们在Verify的时候通过认证知道了用户的身份信息(比如uid),我们希望把这个uid存起来,这样可以在业务逻辑里使用。
因此我们提供了SetKey()、GetKey()两个函数,用于存储请求上下文:
比如认证通过后我们可以设置UID到上下文,然后在reqFunc()里读取设置到req里面(下面介绍)。
// 认证
func Verify(c *gin.Context) {
// 认证处理
// ...
// 忽略认证的具体逻辑
ginrest.SetKey(c, KeyUserID, uid)
}
请求结构体处理
上面我们设置了请求上下文,比如UID,但是其实我们并不知道具体这个UID是需要设置到req里的哪个字段,因此我们提供了一个回调函数ReqFunc(),用于设置Req:
// 这里↓
reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) {
req.UID = ginrest.GetKey[int](c, KeyUserID)
}
// 认证,绑定UID,处理
e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
注
如果这个库的设计不符合具体的业务,也可以按照这种思路去封装一个类似的库,只要尽可能的统一请求、响应的格式,就可以减少很多重复的模板代码。
来源:https://juejin.cn/post/7132790934353215502
猜你喜欢
- 这就意味着数据库和表名在 Windows 中是大小写不敏感的,而在大多数类型的 Unix 系统中是大小写敏感的。一个特例是 Mac OS X
- 可以使用条件断点,如图,在断点上右键可以设置,条件自己输入,python语法:来源:https://blog.csdn.net/daijig
- 开发工具**Python版本:**3.6.4相关模块:pyecharts模块;以及一些Python自带的模块。环境搭建安装Python并添加
- Python是一种计算机程序设计语言,一种面向对象的动态类型语言,一种脚本语言。最初被设计用于编写自动化脚本(shell)的,常用于各种服务
- 本文实例为大家分享了pygame贪吃蛇游戏的具体代码,供大家参考,具体内容如下1.准备工作我们已经初始化了一个400*400的界面,为方便看
- 页面跳转页面跳转的url中必须在最后会自动添加【\】,所以在urls.py的路由表中需要对应添加【\】from django.shortcu
- Jupyter Notebook读取csv文件失败1.IndentationError: expected an indented bloc
- 安装 pip install django-crontab在Django项目中使用settings.pyINSTALLED_AP
- 导语:用 Python 读取图片的像素值,然后输出到 Excel 表格中,最终形成一幅像素画,也就是电子版的十字绣了。基本思路实现这个需求的
- 1.删除原有的mariadb,不然mysql装不进去mariadb-libs-5.5.52-1.el7.x86_64rpm -qa|grep
- 本文实例讲述了Python迭代器与生成器用法。分享给大家供大家参考,具体如下:迭代器,迭代的工具什么是迭代器?指的是一个重复的过程,每一次重
- 今天遇到一个需要用javascript将url中的某些参数替换的需求,想起了不久前从网上淘到了一个parseUrl函数,正好可以借此实现,代
- 群里有人提出这么一个需求:每天都会传过来一份 Word 文档,里面有多个 Excel 附件,需要把 Excel 内容读取出来。第一反应是使用
- 这篇文章主要介绍了Python hashlib模块加密过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,
- 最近整理了一下网上关于MySql 链接url 参数的设置,有不正确的地方希望大家多多指教: mysql JDBC URL格式如下: jdbc
- 字符串和切片(string and slice)string底层就是一个byte的数组,因此,也可以进行切片操作。package maini
- 前面简单介绍了Python字符串基本操作,这里再来简单讲述一下Python列表相关操作1. 基本定义与判断>>> dir(
- 小毅的blog:http://andymao.com/andy/注:本文实例在IE5.x下可能会显示不出来,请使用IE6、IE7、Firef
- 目录一、前言二、方法1、代码2、运行一、前言SpringBoot作为后端开发框架,有强大且方便的处理能力。但是作为一个结合数据分析+前台展示
- 好不容易有些空余时间,便拿来写自己的CSS选择器引擎了,这个CSS选择器引擎的目标只有三个:速度要快代码要精简要支持CSS3的选择器。希望通