Iris - 参数校验器

Iris 2020-01-30 5506 字 1530 浏览 点赞

起步

Iris - 向后端传参 一篇中讲述了后端如何接收前端传递过来的参数,这一篇则准备讲述后端如何校验这些参数。

Iris 官方使用案例 借助 validator 进行参数校验,因而此篇与 Iris 框架关系不大,但又是 Web 开发中较为重要一环。

环境准备

由于涉及到前端的参数传递,我们需要准备以下前端代码:

fetch("http://127.0.0.1:8080/form", {
  body: JSON.stringify({
    // 数据内容
  }),
  method: "POST",
})
.then(resp => resp.json())
.then(data => console.log(data))

这是一段向后端发起 POST 请求,数据格式为 json 的 js 代码。//数据内容 表示需要传递的数据,是后续实验中唯一需要修改部分。

代码可以在 chrome console 中运行,亦可写入 html 文件中,不再累述。


后端代码需要设置允许跨域访问,这里我打算用中间件解决。

app.Use(func(ctx iris.Context) {
    ctx.Header("Access-Control-Allow-Origin", "*")
    ctx.Next()
})

普通参数校验

首先我们需要想明白一个问题:为什么 Iris 要借助三方库?大多时候,这类问题不消细想答案就有两个:1. 代码复用 2. 代码模块化。如果不使用三方库、官方库,应该如何进行参数校验?下边举个例子。

例:要求学生名字 不低于 3 个字母,年龄 不低于 15 岁,性别要么男要么女

// Student 学生
type Student struct {
    Name   string `json:"name"`
    Age    int8   `json:"age"`
    Gender string `json:"gender"`
}

// ValidateStudent 校验 Student
func ValidateStudent(s Student) bool {
    // validate name
    if len(s.Name) < 3 {
        return false
    }
    // validate age
    if s.Age < 15 {
        return false
    }
    // validate gender
    if s.Gender != "girl" && s.Gender != "boy" {
        return false
    }
    return true
}

func main() {
    ...
    app.Post("/form", func(ctx iris.Context) {
        var stu Student
        ctx.ReadJSON(&stu)
        if isInvalid := ValidateStudent(stu); !isInvalid {
            ctx.JSON(iris.Map{"error": "参数无效"})
        } else {
            ctx.JSON(iris.Map{"msg": "参数有效"})
        }
    })
    ...
}

js 测试代码只需要修改数据部分:

body: JSON.stringify({
  name: "zhong",
  age: 15,
  gender: "girl"
}),

显然,ValidateStudent 这一类参数检查的代码是比较累赘的存在,复用率、灵活性不高,你需要对 Student 写一个检查器,如果有 Teacher 结构体你又要写一个检查器。

当然你可以将 ValidateStudent 函数进行拆分,参数校验时使用方式如下:

if isInvalid := ValidateName(stu.Name); !isInvalid {
    return false
} else {
    return true
}
// 同上操作
ValidateAge(stu.Age)  ...
// 同上操作
ValidateGender(stu.Gender)  ...

这样一来通过组合的方式,可以最大化实现代码复用。貌似还不错。但缺点是,你已经在不知不觉地“造轮子”了,这“轮子”还很 low。

Validator 入门

还是以上面那个例子,用 Validator 如何实现呢?

你需要引入 validator 包:

import "gopkg.in/go-playground/validator.v9"

代码实现如下:

// Student 学生
type Student struct {
    Name   string `json:"name" validate:"gte=3"`
    Age    int8   `json:"age" validate:"gte=15"`
    Gender string `json:"gender" validate:"eq=girl|eq=boy"`
}

func main() {
    ...
    // 实例一个检查器对象
    validate := validator.New()
    app.Post("/form", func(ctx iris.Context) {
        var stu Student
        ctx.ReadJSON(&stu)
        // 进行参数检查
        if err := validate.Struct(stu); err != nil {
            ctx.JSON(iris.Map{"error": err.Error()})
        } else {
            ctx.JSON(iris.Map{"msg": "参数有效"})
        }
    })
    app.Run(iris.Addr(":8080"))
}

validator 使用方法也是基本的三步骤。第一步:实例化一个对象(validator.New());第二步:以 tag 方式对结构体中的字段做限制(validate:"gte=15");第三步:启动检查(validate.Struct())。

以上使用的 gte(大于等于)eq(等于) 都是内嵌函数,| 表示“或”,是 validator 能够自动解析的。更多内嵌函数以及用法可参看 package validator。本系列是 Iris 框架,不宜扩展太远,但有机会会把这一部分补充完整。

自定义 Validator Tag

validator 提供的内嵌函数是不够用的,借助 RegisterValidation 可实现自定义 tag。

换句话说,validate:"eq=girl|eq=boy" 我们看起来很累赘,现在用标签 gender 替换掉。

type Student struct {
    ...
    Gender string `json:"gender" validate:"gender"`
}

validate.RegisterValidation("gender", func(fl validator.FieldLevel) bool {
    // 获取 Field 的值
    gender := fl.Field().String()
    if gender != "girl" && gender != "boy" {
        return false
    }
    return true
})
  • RegisterValidation 方法的第一个参数是标签名,第二个是检查逻辑,也就是一个返回 bool 值的函数。
  • fl.Field() 返回值为 reflect.Value 类型,这涉及到了 Go 语言的反射,不做细讲。

如果你想给标签传递参数,如同eq=girl,可以仿照 eq 的实现方式:

// eq 源码
func isEq(fl FieldLevel) bool {

    field := fl.Field()
    param := fl.Param()  // 获取参数
    
    switch field.Kind() {  // 根据值的类型做相应处理
        ...
    }

    panic(fmt.Sprintf("Bad field type %T", field.Interface()))
}

事实上也不需要大动干戈地重写检查逻辑,RegisterAlias 方法允许你为复杂的标签起别名。上述代码可以改写为以下方式:

type Student struct {
    ...
    Gender string `json:"gender" validate:"gender"`
}

func main() {
    app := iris.Default()

    validate := validator.New()
    // 注册别名
    validate.RegisterAlias("gender", "eq=girl|eq=boy")
    ...
}

自定义 Validation Function

Validator Tag 针对的是字段(Field),而 Validation Function 针对的是结构体;前者控制颗粒度更细,后者较粗。一小一大,面面俱到。

type Student struct {
    ...
    Gender string `json:"gender"`
}

func StudentValidationFunc(sl validator.StructLevel) {
    // stu 是 Student 类型
    stu := sl.Current().Interface().(Student)
    // 检查逻辑
    if stu.Gender != "girl" && stu.Gender != "boy" {
        // 不规范的类型以 ReportError 方法报错
        sl.ReportError(stu.Gender, "Gender", "gender", "", "")
    }
}

func main() {
    app := iris.Default()

    validate := validator.New()
    // 注册结构体检查器
    validate.RegisterStructValidation(StudentValidationFunc, Student{})
    ...
    app.Run(iris.Addr(":8080"))
}
  • RegisterStructValidation 方法第一个参数是检查方法,第二个参数是结构体类型的值
  • sl.Current() 返回 reflect.Value 类型,相应使用也涉及到反射知识。

你会不会觉得这同我们最初自己实现的 ValidateStudent 没什么区别呀!但是你别忘记,当你需要做参数校验时,validator 能够提供一致的检查风格 validate.Struct,手动实现的则需要调用不同的函数。一旦你想要将它们整齐划一,你就陷入造轮子的陷阱里去。

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论