写在前面

如果你现在在用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(&params); 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项目示例。

接下来,我们介绍如何优化参数处理方式。实现请求参数的自动装配。

实现参数自动绑定

在实现参数自动绑定之前,我们需要先考虑以下两个问题。

  1. 针对GET、POST、PUT、DELETE等不同请求方式,我们读取参数的规范如何制定

  2. 针对一个已有的项目,如何实现自动绑定对原有系统造成的改动影响最小

带着这两个问题,我们来看看具体如何实现

上面代码示例中,使用c.ShouldBindQuery(&params)来将请求localhost:8080/api/v1/hello?name=seven的参数name=seven绑定到params对象中。如果你有使用Gin的经验,gin还提供一个参数绑定的方法c.ShouldBindJSON可以用来绑定body参数。当然还有其他的诸如BindHeaderBindUri方法,本文暂不涉及。我们用c.ShouldBindQueryc.ShouldBindJSON来实现CURD基本方法的参数自动绑定。

先解决问题1,我们在这里定义规范GETDELETE方法参数放置在请求路径上,如:localhost:8080/api/v1/hello?name=sevenPOSTPUT方法请求参数放在body体内。针对GETDELETE我们使用c.ShouldBindQuery来绑定参数,针对POSTPUT我们使用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实现参数自动绑定的思路。实现方案简单可行,可用于已有系统的改造。如果你有更好的实现思路,不妨评论中,分享一下。最后,今天圣诞节,祝大家圣诞节快乐~~。