Recently, WHEN I learned GIN, I found that the verification of request parameters was very troublesome and there were a lot of repeated codes. After some thinking and practice, I found a method using reflection to realize automatic extraction of request parameters to the specified struct on the controller function. Validation is automatically performed with Validation.

The cause of

Here’s a very common piece of code for handling login, getting the request parameters, verifying that the parameters are correct and returning an error code in case of an error, this kind of code is very common in projects, it’s very repetitive, and it’s always boring to write.

type LoginParam struct {
	Account  string `validate:"required" json:"account"`
	Password string `validate:"required" json:"password"`
}

func main(a){
  g := gin.New()
  g.POST("login", LoginHandlerFunc)
  _ = g.Run(": 8080")}// HanlderFunc
func LoginHandlerFunc(ctx *gin.Context) {
    param := LonginParam{}
    err := ctx.ShouldBind(&param)
    iferr ! =nil {
    ctx.String(400."Request parameter exception")
        return
    }
    secc, msg := Validate(&param)
    if! secc { ctx.String(400, msg)
        return
    }
    // ... CRUD
}
Copy the code

The following is the validator, omitting the interpreter registration code and so on.

import (
	"github.com/go-playground/validator/v10"
)

var v = validator.New()

func Validate(param interface{}) (bool.string) {
    err := v.Struct(param)
    errs := err.(validator.ValidationErrors)
    if len(errs) > 0 {
        err := errs[0]
        return false, err.Translate(that.trans)
    }
    return true.""
}
Copy the code

The target

The mapping parameters and validation parameters are fixed, and the goal is to optimize LoginHandlerFunc as shown below.

func LoginHandlerFunc(ctx *gin.Context, params *LonginParam) {
    // ... CRUD
}
Copy the code

Train of thought

Question 1: How to achieve automatic creation according to function parameters

I have no way of knowing the exact type of struct mapped to the request parameters, so I can only use reflection to get the parameters of the function, as follows.

func reflectHandlerFunc(handlerFunc interface{}){
	funcType := reflect.TypeOf(handlerFunc)
	// check whether Func exists
	iffuncType.Kind() ! = reflect.Func {panic("the route handlerFunc must be a function")}// Get the type of the second argument
	typeParam := funcType.In(1).Elem()
	// Create an instance
	instance := reflect.New(typeParam).Interface()
}
Copy the code

As above, you can get Type by reflecting handlerFunc and taking the second argument to the function and then creating the instance by reflecting it.

Question 2: How to map parameters and validate

Optimize the code in question 1.

func proxyHandlerFunc(ctx *gin.Context, handlerFunc interface{}){
	funcType := reflect.TypeOf(handlerFunc)
  funcValue := reflect.ValueOf(handlerFunc)

	// check whether Func exists
	iffuncType.Kind() ! = reflect.Func {panic("the route handlerFunc must be a function")}// Get the type of the second argument
	typeParam := funcType.In(1).Elem()
	// Create an instance
	param := reflect.New(typeParam).Interface()
	// Bind parameters to struct
	err := ctx.ShouldBind(&param)
	iferr ! =nil {
		ctx.String(400."Request parameter exception")
		return
	}
	// Validate parameters
	succ, msg := Validate(&param)
	if! succ { ctx.String(400, msg)
		return
	}
	// Call the real HandlerFunc
	reflect.Call(valOf(ctx, param))
}

func valOf(i ...interface{}) []reflect.Value {
	var rt []reflect.Value
	for _, i2 := range i {
		rt = append(rt, reflect.ValueOf(i2))
	}
	return rt
}
Copy the code

This completes a proxy for HandlerFunc, as long as we wrap the real HandlerFunc when we register the route.


// ...
g.POST("login", getHandlerFunc(LoginHandlerFunc))
// ...

func getHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
	return func(context *gin.Context){
		proxyHandlerFunc(context, handlerFunc)
	}
}

// ...

func LoginHandlerFunc(ctx *gin.Context, param *LoginParam){
		// ... CRUD
}
Copy the code

Code and performance optimization

0. Compatible with original HandlerFunc

Let’s say I’m halfway through the project and I can’t refactor all of HandlerFunc in one step, I need to be compatible with the original method, which is a simple judgment call.

func getHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
	// Get the number of parameters
	paramNum := reflect.TypeOf(handlerFunc).NumIn()
	valueFunc := reflect.ValueOf(handlerFunc)
	return func(context *gin.Context){
		// Only one parameter description is unrefactored HandlerFunc
		if paramNum == 1 {
				valueFunc.Call(valOf(context))
				return
		}
		proxyHandlerFunc(context, handlerFunc)
	}
}
Copy the code

1. Manually bind and verify specific parameters

In actual development, some interfaces may be forms, some are JSON, and some are other types. The above code can only be handled automatically by gin.ShouldBind to bind to struct process. If it is implemented, we will use the method of the interface to bind, and the specific implementation is as follows.

type Deserialzer interface {
	DeserializeFrom(ctx *gin.Context) error
}

type LoginParam struct {
	Account  string `validate:"required" json:"account"`
	Password string `validate:"required" json:"password"`
}

func (that *LoginParam) DeserializeFrom(ctx *gin.Context) error {
		return ctx.ShouldBindWith(that, binding.FormPost)
}
Copy the code

After the above modification, we give the specific binding process to the specific structs themselves, and customize the binding for all structs that implement the Dserializer interface. After that, we only need to make a little change to proxyHandlerFunc to achieve the adaptation of this function.

func proxyHandlerFunc(ctx *gin.Context, handlerFunc interface{}){
	// ...
	// Create an instance
	param := reflect.New(typeParam).Interface()
	deser, ok := param.(Deserialzer)
	// If the Deserializer interface is not implemented, the default binding process is used for this struct.
	if! ok {// Bind parameters to struct
		err := ctx.ShouldBind(&param)
		iferr ! =nil {
			ctx.String(400."Request parameter exception")
			return}}else {
		// Bind request parameters
		err := deser.DeserializeFrom(ctx)
		iferr ! =nil {
			ctx.String(400."Request parameter exception")
			return
		}
		param = reflect.ValueOf(deser).Interface()		
	}
	// Validate parameters
	succ, msg := Validate(&param)
	// ...
}
Copy the code

The same is true for the custom parameter validation process.

Performance optimization

GO’s reflection has a huge impact on performance, so you should avoid using reflection in HandleFunc. The time difference between using reflection and not using reflection is about 300 times. Therefore, some types and values can be reflected during route registration to avoid reflection every time they are used.

As shown below, we pre-reflect the actual handlerFunc, as well as the parameter types.

func GetHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
	// Reflect ahead
	paramNum := reflect.TypeOf(handlerFunc).NumIn()
	funcValue := reflect.ValueOf(handlerFunc)
	funcType := reflect.TypeOf(handleFunc)
	paramType := funcType.In(1).Elem()

	// check whether Func exists
	iffuncType.Kind() ! = reflect.Func {panic("the route handlerFunc must be a function")}/ /... And you can do some other checks to make sure it's right
	return func(context *gin.Context){
		// Only one parameter description is unrefactored HandlerFunc
		if paramNum == 1 {
				funcValue.Call(valOf(context))
				return
		}
		proxyHandlerFunc(context, funcValue, paramType)
	}
}

func proxyHandlerFunc(ctx *gin.Context, funcValue reflect.Value, typeParam reflect.Type){
	// Create an instance
	param := reflect.New(typeParam).Interface()
	// ...
	// Call the real HandlerFunc
	reflect.Call(valOf(ctx, param))
}

func valOf(i ...interface{}) []reflect.Value {
	var rt []reflect.Value
	for _, i2 := range i {
		rt = append(rt, reflect.ValueOf(i2))
	}
	return rt
}
Copy the code

The performance test

Baenchmark was used for performance tests.

func BenchmarkNormalHandleFunc(b *testing.B) {
	router := gin.New()
	router.POST("login".func(ctx *gin.Context) {
		p := validates.RegisterParams{}
		validator := LoginParam{}
		if! validator.Validate(wrap.Context(ctx), &p) {return
		}
	})
	config.Router = router
	go func(a) {
		_ = router.Run(": 8081")
	}()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		testPost("8081")}}func BenchmarkReflectHandleFunc(b *testing.B) {
	router := gin.New()
	handlerFunc := func(ctx *gin.Context, params *LoginParam) {
		/ /...
	}
	router.POST("login", GetHandlerFunc(handleFunc))
	go func(a) {
		_ = router.Run(": 8082")
	}()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		testPost("8082")}}func testPost(port string) {
	params := struct {
		Account  string
		Password string
		Email    string
		Captcha  string
	}{
		Account:  "account",
		Password: "1231ljasd",
		Email:    "[email protected]",
		Captcha:  "12345",
	}
	paramsByte, _ := json.Marshal(params)
	r, _ := http.Post("http://127.0.0.1:"+port+"/login"."application/json", bytes.NewReader(paramsByte))

	ifr.StatusCode ! =200 {
		fmt.Println("errr")}}Copy the code

The test results are as follows.

E:\go> go test -bench="." -count=5 -benchmem goos: windows goarch: amd64 pkg: go BenchmarkNormalHandleFunc-12 19603 59545 ns/op 7321 B/op 83 allocs/op BenchmarkNormalHandleFunc-12 17412 66923 ns/op 7306 B/op 83 allocs/op BenchmarkNormalHandleFunc-12 20368 57849 ns/op 7349 B/op 83 allocs/op BenchmarkNormalHandleFunc-12 20542 60086 ns/op 7395 B/op 83 allocs/op BenchmarkNormalHandleFunc-12 20577 58671 ns/op 7361 B/op 83 allocs/op BenchmarkReflectHandleFunc-12 20613 58374 ns/op 7493 B/op 85 allocs/op BenchmarkReflectHandleFunc-12 20230 62594 ns/op 7456 B/op 85 allocs/op BenchmarkReflectHandleFunc-12 19764 59617 ns/op 7441 B/op 85 allocs/op BenchmarkReflectHandleFunc-12 20684 58899 ns/op 7461 B/op 85 allocs/op BenchmarkReflectHandleFunc - 12, 19796, 58712 ns/op 7421 B / 85 allocs op/op PASS ok go 18.177 sCopy the code

Performance suffers almost no loss.

The above method is my practice in specific projects: GitHub