параметры автоматического сопоставления джина и автоматическая проверка

Go

Когда я недавно изучал джин, то обнаружил, что проверка параметров запроса была очень хлопотной, и было много повторяющихся кодов.После некоторого размышления и практики я нашел метод с использованием рефлексии, который реализовал автоматическое извлечение параметров запроса на функцию контроллера для указанной структуры и автоматически использовать проверку для проверки.

причина

Как показано ниже, это очень распространенный код для обработки входа в систему, получения параметров запроса, проверки правильности параметров и возврата кода ошибки, если они неверны.Этот тип кода очень распространен в проектах и ​​очень часто повторяется. такой код всегда раздражает.

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

func main(){
  g := gin.New()
  g.POST("login", LoginHandlerFunc)
  _ = g.Run(":8080")
}

// HanlderFunc
func LoginHandlerFunc(ctx *gin.Context) {
    param := LonginParam{}
    err := ctx.ShouldBind(&param)
    if err != nil {
    ctx.String(400, "请求参数异常")
        return
    }
    secc, msg := Validate(&param)
    if !secc {
        ctx.String(400, msg)
        return
    }
    // ... CRUD
}

Далее идет валидатор, опускающий код типа регистрации переводчика.

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, ""
}

Цель

Параметры отображения и параметры проверки фиксированы, и цель состоит в том, чтобыLoginHandlerFuncОптимизировано, как показано ниже.

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

идеи

Вопрос 1: Как реализовать автоматическое создание на основе параметров функции

Я не могу знать конкретный тип структуры, отображаемой параметрами запроса, поэтому я могу использовать отражение только для получения параметров функции путем отражения.Конкретная практика заключается в следующем.

func reflectHandlerFunc(handlerFunc interface{}){
	funcType := reflect.TypeOf(handlerFunc)
	// 判断是否 Func
	if funcType.Kind() != reflect.Func {
		panic("the route handlerFunc must be a function")
	}
	// 获取第二个参数的类型
	typeParam := funcType.In(1).Elem()
	// 创建实例
	instance := reflect.New(typeParam).Interface()
}

Как и выше, вы можете получить его, отразив handlerFunc, а затем получив второй параметр функцииTypeЗатем создайте экземпляр с помощью отражения.

Вопрос 2: Как сопоставить параметры и проверить

Оптимизируйте код в вопросе 1.

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

	// 判断是否 Func
	if funcType.Kind() != reflect.Func {
		panic("the route handlerFunc must be a function")
	}
	// 获取第二个参数的类型
	typeParam := funcType.In(1).Elem()
	// 创建实例
	param := reflect.New(typeParam).Interface()
	// 绑定参数到 struct
	err := ctx.ShouldBind(&param)
	if err != nil {
		ctx.String(400, "请求参数异常")
		return
	}
	// 验证参数
	succ, msg := Validate(&param)
	if !succ {
		ctx.String(400, msg)
		return
	}
	// 调用真实 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
}

Таким образом, работа прокси для HandlerFunc завершена, пока мы обертываем настоящий HandlerFunc при регистрации маршрута.


// ...
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
}

Код и оптимизация производительности

0. Совместимость с оригинальной HandlerFunc

Предполагая, что проект находится на полпути, и я не могу реорганизовать все HandlerFunc за один шаг, мне нужно быть совместимым с исходным методом, который должен только добавить простое суждение.

func getHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
	// 获取参数数量
	paramNum := reflect.TypeOf(handlerFunc).NumIn()
	valueFunc := reflect.ValueOf(handlerFunc)
	return func(context *gin.Context){
		// 只有一个参数说明是未重构的 HandlerFunc
		if paramNum == 1 {
				valueFunc.Call(valOf(context))
				return
		}
		proxyHandlerFunc(context, handlerFunc)
	}
}

1. Вручную привязать и проверить определенные параметры

В реальной разработке некоторые интерфейсы могут быть формами, некоторые интерфейсы — JSON, а некоторые — другими типами.Приведенный выше код может использоваться толькоgin.ShouldBindАвтоматически обрабатывать процесс привязки к структуре, для этой проблемы дайтеParamДостаточно реализовать определенный интерфейс.Если он реализован, мы используем метод интерфейса для привязки.Конкретная реализация выглядит следующим образом.

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)
}

После приведенного выше преобразования мы передаем конкретный процесс привязки самой конкретной структуре для всех реализаций.DserializerВсе структуры интерфейса связаны пользовательскими настройками, и тогда толькоproxyHandlerFuncДля достижения этой адаптации можно внести небольшое изменение.

func proxyHandlerFunc(ctx *gin.Context, handlerFunc interface{}){
	// ...
	// 创建实例
	param := reflect.New(typeParam).Interface()
	deser, ok := param.(Deserialzer)
	// 如果未实现 Deserializer 接口则说明该 struct 使用默认绑定过程即可.
	if !ok {
		// 绑定参数到 struct
		err := ctx.ShouldBind(&param)
		if err != nil {
			ctx.String(400, "请求参数异常")
			return
		}
	} else {
		// 绑定请求参数
		err := deser.DeserializeFrom(ctx)
		if err != nil {
			ctx.String(400, "请求参数异常")
			return
		}
		param = reflect.ValueOf(deser).Interface()		
	}
	// 验证参数
	succ, msg := Validate(&param)
	// ...
}

Тот же метод можно использовать для процесса проверки пользовательских параметров.

оптимизация производительности

Влияние отражения в GO на производительность огромно, поэтому старайтесь избегать использования отражения в HandleFunc. Разница во времени между приведенными выше функциями, использующими отражение и не использующими отражение, составляет примерно 300 раз.Type, ValueОтражение можно выполнять при регистрации маршрута, а предварительное отражение позволяет избежать отражения каждый раз, когда оно используется.

Как показано ниже, мы заранее размышляем над реальной функцией handlerFunc и типом параметра.

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

	// 判断是否 Func
	if funcType.Kind() != reflect.Func {
		panic("the route handlerFunc must be a function")
	}
	// ... 还可以做一些其他校验确保无误
	return func(context *gin.Context){
		// 只有一个参数说明是未重构的 HandlerFunc
		if paramNum == 1 {
				funcValue.Call(valOf(context))
				return
		}
		proxyHandlerFunc(context, funcValue, paramType)
	}
}

func proxyHandlerFunc(ctx *gin.Context, funcValue reflect.Value, typeParam reflect.Type){
	// 创建实例
	param := reflect.New(typeParam).Interface()
	// ...
	// 调用真实 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
}

Тестирование производительности

Используйте Baenchmark для тестирования производительности.

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() {
		_ = 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() {
		_ = 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@exmpale.com",
		Captcha:  "12345",
	}
	paramsByte, _ := json.Marshal(params)
	r, _ := http.Post("http://127.0.0.1:"+port+"/login", "application/json", bytes.NewReader(paramsByte))

	if r.StatusCode != 200 {
		fmt.Println("errr")
	}
}

Результаты испытаний следующие.

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/op         85 allocs/op
PASS
ok      go    18.177s

Потери производительности почти нет.

Вышеуказанные методы я практикую в конкретных проектах:GitHub