目前项目里面参数校验和业务逻辑是耦合在一起的,感觉不是很清晰,看名称是参数校验,其实内部包含业务逻辑,例如下面的代码
vApi.GET("news", validatorFactory.Create(consts.ValidatorPrefix+"HomeNews"))
事实上validatorFactory.Create
返回的是个参数校验器,但是目前参数校验器内部又会去调用真正的业务逻辑。
我觉得可以参考spring mvc的做法,参数校验也设计成一个middleware,利用gin依赖的validator工具来处理参数校验,当校验通过后,通过context.Next()
调用真正的业务逻辑。
表单参数验证器和控制器的调用的确是存在一点代码耦合,这是个已知问题。
gin的路由参数支持63个回调函数,
如果按照以下规则设置:
第一个逻辑 表单参数验证器代码对参数进行校验,通过以后,进行下一个函数回调。
第二个逻辑 调用控制器相关代码.
代码如下:
users.GET(
"index",
validatorFactory.Create(consts.ValidatorPrefix+"UsersShow"),
(&web.Users{}).Show
)
这样同样也可以降低耦合度. 不知道是否符合你的预期?
只不过这样的方式如果实际业务当中,控制器存在比较多的Aop(业务切面),的话,就会导致路由的import快速增多。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
2020.11.27 更新增加了主线解耦文档,
你可以参考一下,自己再实际使用时,可以自行进行解耦:
https://gitee.com/daitougege/GinSkeleton/blob/master/docs/low_coupling.md
看了解耦的代码,我觉得基本是认同的。我今天也在思考解耦的问题,我这边主要是参考了spring mvc中controller的思路,controller函数通过注解后,函数的签名已经完全是和业务相关的,可以不包含http的相关信息,例如怎么序列化body,怎么验证body等,我觉得这种方式可以让开发者专注,于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...)
,cxt.Query(...)
这种代码几乎每个handler里面都要写,你这边的设计,我觉得没有根本改变这种模式,只是减少了代码量。我自己写了个demo,也是通过反射和约定,做了一个简化的代理,让我们能比较灵活的,更加专注于业务的方式定义handler,你这边有时间的话,也可以参考一下。只是一个想法,也许方向就是错误的,如果你有意见和建议,欢迎指正。
package main
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"reflect"
"time"
)
func main() {
route := gin.Default()
route.POST("user", ginHandlerWrapper(userHandler, 4))
route.Run(":8080")
}
//-------------------------------------
// 业务逻辑包装函数
// f是一个函数,是真正的处理业务逻辑的handler
// mode代表入参的信息,我们约定入参的第一个参数是*gin.Context,后面最多有三个参数,按照约定的顺序分别是body,query和header
// mode最后三位如果是1就表示有一个对应入参,否则没有,例如mode=0x101,则表示handler函数是f(*gin.Context, *bodyStruct{}, *headerStruct{})
// 返回值我们约定是(data interface{}, err error)
// 这样处理的好处是:
// 1. 我们可以统一的处理参数校验,返回的数据封装,返回的错误处理
// 2. 一定程度的自由定义handler函数,handler函数的入参都是真正要处理的业务参数,e.g. body,使其进一步和gin解耦,不依赖gin的context;这一点我是参考spring mvc中controller的设计
func ginHandlerWrapper(f interface{}, mode uint8) (hf gin.HandlerFunc) {
ft := reflect.TypeOf(f)
if ft.Kind() != reflect.Func {
panic("not func")
}
in := make([]*inParamsInfo, 0)
var j uint8 = 4
index := 1 // 第一个参数默认是context
for i := 0; i < 3; i++ {
if mode&j == j {
pt := ft.In(index)
pt = pt.Elem()
param := reflect.New(pt).Interface()
in = append(in, &inParamsInfo{
typ: info[i],
value: param,
})
index++
}
j = j >> 1
}
fv := reflect.ValueOf(f)
hf = func(cxt *gin.Context) {
inParams := make([]reflect.Value, 0)
inParams = append(inParams, reflect.ValueOf(cxt))
var e error
for _, pi := range in {
switch pi.typ {
case body: // body
e = cxt.BindJSON(pi.value)
case query: // query
e = cxt.BindQuery(pi.value)
case header: // header
e = cxt.BindHeader(pi.value)
}
if e != nil {
cxt.JSON(400, "invalid params")
return
}
inParams = append(inParams, reflect.ValueOf(pi.value))
}
results := fv.Call(inParams)
if results[1].Interface() != nil {
cxt.JSON(500, "internal error")
return
}
cxt.JSON(200, &CommonResponse{
Code: 0,
Info: "",
Data: results[0].Interface(),
Timestamp: time.Now().Unix(),
})
}
return
}
const (
body uint8 = iota
query
header
)
// 入参顺序
var info = []uint8{body, query, header}
type inParamsInfo struct {
typ uint8 // 入参类型
value interface{}
}
// 统一返回
type CommonResponse struct {
Code int
Info string
Data interface{}
Timestamp int64
}
//-------------------------------------------------
// 处理业务逻辑的handler
type user struct {
Name string `json:"name" binding:"required,min=6,max=20"`
Age int `json:"age" binding:"required"`
}
func userHandler(cxt *gin.Context, u *user) (data interface{}, err error) {
bs, _ := json.Marshal(u)
fmt.Printf("user: %s\n", bs)
data = &user{
Name: u.Name + "!!!",
Age: u.Age + 1,
}
return
}
多谢,周末仔细看一下你的思路...
@zhoudaxiang
我周末运行了一下你这段代码,先说目前我个人关注的几个小的问题,也不一定都是真正的问题,仅供大家探讨一下:
1.ginHandlerWrapper 该函数的代码段应该放置在 hf = func(cxt *gin.Context) { ... } 之间,否则函数被注册的时候就直接执行了,该函数应该是请求到达时才执行。
2.业务处理函数,如果除了 context 参数之外,约定3个参数,就缺少了 form-data 、 x-www-form-urlencoded 类型的参数,盎然这个问题可以通过扩展参数个数补充进去。
3. 你说的"于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...),cxt.Query(...)这种代码几乎每个handler里面都要写"。 按照goskeleton目前的设计,第一层逻辑表单参数验证之后,参数就自动绑定了上下文 *gin.context 上面,后续的任何地方都直接按照json标签对应的键直接获取,没有你说的这个情况。
4.这种模式虽然对代码进行了解耦,但是每个回调函数都需要反射一次,相比 上下文 *gin.context 性能肯定低不少,gin的 context 就是一个 0x开头的10位指针,由该指针贯穿整个request到response的生命周期,所有主线数据都绑定在 context 上,这样做在性能和易于理解上都占有优势。
其他方面:
1.你这个模式除了你说的2点优势之外,gin-swagger包 https://github.com/swaggo/gin-swagger 也用到了包裹模式,肯定是具备某些优势的。
2.这个思想先保留着,看看在实应用中如何优雅地贯穿进去。
@zhoudaxiang
我周末运行了一下你这段代码,先说目前我个人关注的几个小的问题,也不一定都是真正的问题,仅供大家探讨一下:
1.ginHandlerWrapper 该函数的代码段应该放置在 hf = func(cxt *gin.Context) { ... } 之间,否则函数被注册的时候就直接执行了,该函数应该是请求到达时才执行。
2.业务处理函数,如果除了 context 参数之外,约定3个参数,就缺少了 form-data 、 x-www-form-urlencoded 类型的参数,盎然这个问题可以通过扩展参数个数补充进去。
3. 你说的"于业务开发,想想我们现在代码,往往都依赖gin context去获取各种参数,cxt.BindJSON(...),cxt.Query(...)这种代码几乎每个handler里面都要写"。 按照goskeleton目前的设计,第一层逻辑表单参数验证之后,参数就自动绑定了上下文 *gin.context 上面,后续的任何地方都直接按照json标签对应的键直接获取,没有你说的这个情况。
4.这种模式虽然对代码进行了解耦,但是每个回调函数都需要反射一次,相比 上下文 *gin.context 性能肯定低不少,gin的 context 就是一个 0x开头的10位指针,由该指针贯穿整个request到response的生命周期,所有主线数据都绑定在 context 上,这样做在性能和易于理解上都占有优势。
其他方面:
1.你这个模式除了你说的2点优势之外,gin-swagger包 https://github.com/swaggo/gin-swagger 也用到了包裹模式,肯定是具备某些优势的。
2.这个思想先保留着,看看在实应用中如何优雅地贯穿进去。
@张奇峰 非常感谢你的回复,回头我再仔细看看你的建议。目前针对你说的4点,我简单回复下:
ginHandlerWrapper
返回了gin.Handler
,ginHandlerWrapper
也只会在注册时候执行一次;关于校验器解耦,可以参考下faygo框架的思路:https://gitee.com/henrylee/faygo
主要用到了反射,性能可能会有影响,但可读性特别强。
登录 后才可以发表评论