一个永远学不会复制粘贴最佳姿态的小学未毕业生
前言: 在做 web 项目开发过程中, 参数校验代码量基本上等同于真正的业务代码量, 但是参数校验又是毕不可少的一个非常重要的环节, 怎么才能将这些(类似的)校验的逻辑复用呢?
在开始的时候, 我们可能是这样校验参数的
// 定义一个结构体来接受参数
type IndexPost struct {
QueryId int64 `json:"query_id" validate:"required,gte=1"`
Name string `json:"name" validate:"required,ascii"`
}
// 处理 index 业务
func handleIndexPost(ctx *gin.Context) {
var req IndexPost
// 获取参数
if err := ctx.BindJSON(&req); err != nil {
fmt.Println("error", err)
ctx.AbortWithStatusJSON(400, gin.H{"error": "error" + err.Error()})
return
}
// 校验参数
if err := validate.Struct(&req); err != nil {
if canTrans, ok := err.(validator.ValidationErrors); ok {
translate := canTrans.Translate(trans)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params ", "data": translate})
return
} else {
fmt.Errorf("error %s\n", err)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params ", "data": err.Error()})
return
}
}
// 真正处理业务
ctx.AbortWithStatusJSON(200, req)
}
如果再有另外的业务, 我们可能会还是会这么干的, 嗯, 这个简单, 不就是 ctrl + c && ctrl + v 么? 这个好办
// 接受另外的一个请求参数的结构体
type OtherRequest struct {
QueryId int64 `json:"query_id" validate:"required,gte=1"`
Name string `json:"name" validate:"required,ascii"`
}
// 处理另外一个业务的逻辑
func handleOtherPost(ctx *gin.Context) {
var req OtherRequest
// 获取参数
if err := ctx.BindJSON(&req); err != nil {
fmt.Println("error", err)
ctx.AbortWithStatusJSON(400, gin.H{"error": "error" + err.Error()})
return
}
// 校验参数
if err := validate.Struct(&req); err != nil {
if canTrans, ok := err.(validator.ValidationErrors); ok {
translate := canTrans.Translate(trans)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params ", "data": translate})
return
} else {
fmt.Errorf("error %s\n", err)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params ", "data": err.Error()})
return
}
}
// 真正处理业务
ctx.AbortWithStatusJSON(200, req)
}
嗯, 这样好像不怎么好吧, 好多代码都是重复的诶, 已经 "复用" 了很多的代码了啊。。。 这个。。。, 但是这个不是编译单元的代码复用, 万一有一天,上面的代码需要做 i8n 的话, 那就麻烦了, 嗯, 还有只有两个, 修改 2 个地方就好了, 等等, 万一这些 request 有 100 个呢?
如果你已经厌倦了上述的代码方式, 极力追求代码的最简洁, 那么下面就是为了你准备的
怎么能做到上面重复逻辑的复用呢? 在 c ++ 里面是使用抽象类可以做到代码编译单元的复用, 那么使用 golang 怎么才能做到呢? 嗯, 这就要使用 golang 的接口编程了 && 为了要使用在参数校验过程中生成对象, 我们还需要使用反射。
那么我们就定义一个接口, 假设就是叫做参数提取吧. 嗯, 我觉得这个名字够形象了, 起码不用猜, 对吧 😹
type Extractor interface {
Extract(ctx *gin.Context) (err error)
}
有接口, 要实现啊, 好吧, 我们用两个不同类型的 struct 来实现这个接口吧
type IndexRequest struct {
QueryId int64 `json:"query_id" validate:"required,gte=1"`
Name string `json:"name" validate:"required,ascii"`
}
func (c *IndexRequest) Extract(ctx *gin.Context) (err error) {
return ctx.BindJSON(c)
}
// 假设这个参数是否在请求头中的
type ProfileRequest struct {
Token string `json:"-"`
}
func (c *ProfileRequest) Extract(ctx *gin.Context) (err error) {
c.Token = ctx.Request.Header.Get("authorization")
return err
}
诶, 你两个 struct 还是要实现 Extractor 接口, 嗯好吧, 但是, 只需要实现这个接口, 其他都是真正的代码复用了的。。。 你品, 你细品, 你在品。。。
将重复的逻辑包装成一个中间件
func fnRequestFilter(impl Extractor) gin.HandlerFunc {
var val = reflect.TypeOf(impl).Elem()
return func(ctx *gin.Context) {
filter := (reflect.New(val).Interface()).(Extractor)
if err := filter.Extract(ctx); err != nil {
// 处理 picker 请求参数错误的逻辑
ctx.AbortWithStatusJSON(400, gin.H{"error": "bad params"})
return
}
// 使用 validator 来校验字段, 这里全部都是使用反射
if err := validate.Struct(filter); err == nil {
ctx.Set("__request__", filter)
return
} else {
if transErr, ok := err.(validator.ValidationErrors); ok {
translations := transErr.Translate(trans)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params", "data": translations})
return
} else {
fmt.Println("error", err)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params", "data": "unknown error"})
return
}
}
}
}
把中间件用起来:
engine := gin.Default()
engine.POST("/index", fnRequestFilter(&IndexRequest{}), func(ctx *gin.Context) {
// 在这里只写业务相关的代码
data, _ := ctx.Get("__request__")
ctx.JSON(200, data)
})
engine.POST("/profile", fnRequestFilter(&ProfileRequest{}), func(ctx *gin.Context) {
// 在这里只写业务相关的代码
data, _ := ctx.Get("__request__")
ctx.JSON(200, data)
})
if err := engine.Run(":8089"); err != nil {
panic(err)
}
贴一个工程代码:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"reflect"
)
var (
validate = validator.New()
uni = ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
)
func init() {
err := zh_translations.RegisterDefaultTranslations(validate, trans)
if err != nil {
panic(err)
}
}
func main() {
engine := gin.Default()
engine.POST("/index", fnRequestFilter(&IndexRequest{}), func(ctx *gin.Context) {
// 在这里只写业务相关的代码
data, _ := ctx.Get("__request__")
ctx.JSON(200, data)
})
engine.POST("/profile", fnRequestFilter(&ProfileRequest{}), func(ctx *gin.Context) {
// 在这里只写业务相关的代码
data, _ := ctx.Get("__request__")
ctx.JSON(200, data)
})
if err := engine.Run("8089"); err != nil {
panic(err)
}
}
func fnRequestFilter(impl Extractor) gin.HandlerFunc {
var val = reflect.TypeOf(impl).Elem()
return func(ctx *gin.Context) {
filter := (reflect.New(val).Interface()).(Extractor)
if err := filter.Extract(ctx); err != nil {
ctx.AbortWithStatusJSON(400, gin.H{"error": "bad params"})
return
}
if err := validate.Struct(filter); err == nil {
ctx.Set("__request__", filter)
return
} else {
if transErr, ok := err.(validator.ValidationErrors); ok {
translations := transErr.Translate(trans)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params", "data": translations})
return
} else {
fmt.Println("error", err)
ctx.AbortWithStatusJSON(200, gin.H{"error": "invalid params", "data": "unknown error"})
return
}
}
}
}
type Extractor interface {
Extract(ctx *gin.Context) (err error)
}
type IndexRequest struct {
QueryId int64 `json:"query_id" validate:"required,gte=1"`
Name string `json:"name" validate:"required,ascii"`
}
func (c *IndexRequest) Extract(ctx *gin.Context) (err error) {
return ctx.BindJSON(c)
}
// 假设这个参数是否在请求头中的
type ProfileRequest struct {
Token string `json:"-"`
}
func (c *ProfileRequest) Extract(ctx *gin.Context) (err error) {
c.Token = ctx.Request.Header.Get("authorization")
return err
}
其中请求参数实例化和参数校验的过程中使用的反射生成的对象, 要求性能的还是使用普通的方法吧 😢
怎么在编译期间就确定一个接口是否已经被某个 type 实现了呢?
var (
_ Extractor = (*ProfileRequest)(nil) // 这种方式比较常见
)
到这里, 就写完了, 这篇文章想了好久了, 最后祝大家儿童节快乐。。。 🍰 😸