写在前面
如果你现在在用Go-Gin来写web系统,系统没有实现参数自动绑定,强烈建议你花几分钟看完此篇文章。
Gin介绍
在介绍如何实现自动绑定之前,我们来简单复习一下,用Gin来实现一个web系统
项目结构
.
├── app
│ ├── controller
│ │ ├── auto_bind_hello.go
│ │ └── hello.go
│ └── router
│ ├── router.go
│ └── wrap.go
├── cmd
│ └── server
│ └── main.go
├── go.mod
└── go.sum
app/controller存放控制器层代码
app/router存放接口路由代码
cmd/server存放入口函数
介绍完基本结构我们来具体看一下每层的样例代码
app/controller/hello.go
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
)
func Get(c *gin.Context) {
params := struct {
Name string `json:"name" form:"name"`
}{}
if err := c.ShouldBindQuery(¶ms); err != nil {
panic(err)
}
fmt.Println("do something!!")
return
}
在上述样例中,有一个用于处理Get请求的控制器GET
,控制器中有两块逻辑:1. 参数绑定 2. 业务处理。本文要解决的问题就是将第一块逻辑代码公共处理,不需要每一个控制器单独实现。具体方案将第二节介绍。
app/router/router.go
package router
import (
"github.com/gin-gonic/gin"
"github.com/seven-yu/gin-auto-bind/app/controller"
)
func RouteV1(e *gin.Engine) {
apiV1 := e.Group("/api/v1")
{
apiV1.GET("hello", controller.Get)
}
}
router用于注册路由,被main函数引用
cmd/server/main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/seven-yu/gin-auto-bind/app/router"
)
func main() {
e:=gin.Default()
router.RouteV1(e)
err := e.Run(":8080")
if err != nil {
panic(err)
}
}
e.Run(":8080")
之后,一个简单的web系统启动并提供服务。
以上是简单的Gin-web项目示例。
接下来,我们介绍如何优化参数处理方式。实现请求参数的自动装配。
实现参数自动绑定
在实现参数自动绑定之前,我们需要先考虑以下两个问题。
-
针对GET、POST、PUT、DELETE等不同请求方式,我们读取参数的规范如何制定
-
针对一个已有的项目,如何实现自动绑定对原有系统造成的改动影响最小
带着这两个问题,我们来看看具体如何实现
上面代码示例中,使用c.ShouldBindQuery(¶ms)
来将请求localhost:8080/api/v1/hello?name=seven
的参数name=seven
绑定到params对象中。如果你有使用Gin的经验,gin还提供一个参数绑定的方法c.ShouldBindJSON
可以用来绑定body参数。当然还有其他的诸如BindHeader
、BindUri
方法,本文暂不涉及。我们用c.ShouldBindQuery
和c.ShouldBindJSON
来实现CURD基本方法的参数自动绑定。
先解决问题1,我们在这里定义规范GET
和DELETE
方法参数放置在请求路径上,如:localhost:8080/api/v1/hello?name=seven
。POST
和PUT
方法请求参数放在body体内。针对GET
和DELETE
我们使用c.ShouldBindQuery
来绑定参数,针对POST
和PUT
我们使用c.ShouldBindJSON
来进行参数绑定。
我们接下来看问题2,如何实现对项目的侵入性最小。这里介绍一种方法,我们先看下面这个函数。
func AutoBindGet(c *gin.Context, params *GetParams)
我们希望最后的每个controller是类似这种结构,在调用AutoBindGet
方法的时候,已经在中间公共层绑定好了params
照着这个思路往下走,gin 定义的controller处理函数的结构是type HandlerFunc func(*Context)
,我们必须在中间进行一下转换。这个时候就想到了Wrap
方式。
// AutoBindWrap .
// ctrFunc: func AutoBindGet(c *gin.Context, params *GetParams)
func AutoBindWrap(ctrFunc interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 获取ctrFunc函数的参数struct,创建参数实例
// 2. 通过Gin上下文c获取请求方式
// 3. 根据规范,使用c.ShouldBindQuery、获c.ShouldBindJSON 将参数绑定于参数实例
// 4. 调用ctrFunc方法
}
}
以上是wrap
的思路,具体实现我们要用到反射,如果你需要复习一下反射,推荐你看一下反射的原则,下面给出一个简单的wrap
样例。
func AutoBindWrap(ctrFunc interface{}) gin.HandlerFunc {
return func(c *gin.Context) {
ctrType := reflect.TypeOf(ctrFunc)
ctrValue := reflect.ValueOf(ctrFunc)
// 1. check
if ctrType.Kind() != reflect.Func {
panic("not support")
return
}
numIn := ctrType.NumIn()
if numIn != 2 {
panic("not support")
return
}
// 2. bind value
ctrParams := make([]reflect.Value, numIn)
for i := 0; i < numIn; i++ {
pt := ctrType.In(i)
// handle gin.Context
if pt == reflect.TypeOf(&gin.Context{}) {
ctrParams[i] = reflect.ValueOf(c)
continue
}
// handle params
if pt.Kind() == reflect.Ptr && pt.Elem().Kind() == reflect.Struct {
pv := reflect.New(pt.Elem()).Interface()
var err error
switch c.Request.Method {
case http.MethodGet, http.MethodDelete:
err = c.ShouldBindQuery(pv)
case http.MethodPost, http.MethodPut:
err = c.ShouldBindJSON(pv)
}
if err != nil {
panic(err)
return
}
ctrParams[i] = reflect.ValueOf(pv)
}
}
// 3. call controller
ctrValue.Call(ctrParams)
}
}
最后在router中,我们这样注册接口路由
package router
import (
"github.com/gin-gonic/gin"
"github.com/seven-yu/gin-auto-bind/app/controller"
)
func RouteV1(e *gin.Engine) {
apiV1 := e.Group("/api/v1")
{
apiV1.GET("hello", controller.Get)
apiV1.POST("hello", controller.Create)
apiV1.GET("auto_bind_hello", AutoBindWrap(controller.AutoBindGet))
apiV1.POST("auto_bind_hello", AutoBindWrap(controller.AutoBindCreate))
}
}
至此,我们已经完成一个简单的参数绑定公共处理模块。( 是不是很简单,而且很实用 :) )
上述代码只是一个简单实现,提供一个思路,仅作为参考。
本文样例源码地址:https://github.com/seven-yu/gin-auto-bind
小结
本文通过一个样例,介绍了一种基于Gin实现参数自动绑定的思路。实现方案简单可行,可用于已有系统的改造。如果你有更好的实现思路,不妨评论中,分享一下。最后,今天圣诞节,祝大家圣诞节快乐~~。