趁周末写了个小工具 - Golang 实体参数校验器

ormissia · · 1363 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

> A:"请用一句话让别人知道你写过Golang。" > B:"if err!= nil ..." ## 起因 只要是接触过Golang的人,无不为其`if err != nil`的语法感到惊奇,或是大加赞赏,或是狠狠痛批。作为使用者,不管喜欢也好,反对也罢, 目前还是要接受这种错误处理模式。 而最令人头痛的就是请求参数中各种值的校验。比如`Get`请求中接收分页参数时,需要将`string`格式的参数转换成`int`类型,再如时间类型的参数 转换, 诸如此类,等等等等。好家伙,一个接口写完`if err != nil`的判断占了一多半的行数,看着实在不爽。 下面就是一个典型的例子,而且这个接口参数还不是特别多 ```go func Export(c *gin.Context) { //删除开头 //... var param map[string]string err := c.ShouldBindJSON(&param) if err != nil { ErrRsponse(c,errCode) return } var vId, userId, userName, format string if v, ok := param["vId"]; ok { vId = v } else { ErrRsponse(c,errCode) return } if len(vId) == 0 { ErrRsponse(c,errCode) return } if v, ok := param["userId"]; ok { userId = v } else { ErrRsponse(c,errCode) return } if v, ok := param["userName"]; ok { userName = v } else { ErrRsponse(c,errCode) return } if v, ok := param["format"]; ok { format = v } else { ErrRsponse(c,errCode) return } if !file.IsOk(format) { ErrRsponse(c,errCode) return } //... //删除结尾 } ``` ## 机遇 前几天在看[GIN-VUE-ADMIN](https://github.com/flipped-aurora/gin-vue-admin)代码的时候,偶然看到一个通过反射去做参数校验的方式。 嘿,学到了! ## 改变 ### 定义规则 校验规则使用一个`map`存储,`key`为字段名,`value`为规则列表,并使用一个`string`类型的切片来存储。 > 后续计划加入`tag`标签定义规则的功能以及增加通过函数参数的方式,实现自定义规则校验 ```go type Rules map[string][]string ``` 支持的规则有: - 不为空 - 等于、不等于 - 大于、小于 - 大于等于、小于等于 > 对于数值类型为比较值大小,对于字符串或者切片等类型为比较长度大小 比如调用生成小于规则的方法,则会返回一个小于指定值规则的字符串,用于后面校验器使用 ```go // Lt < func (verifier verifier) Lt(limit string) string { return fmt.Sprintf("%s%s%s", lt, verifier.separator, limit) } ``` 规则定义示例: ```go UserRequestRules = go_opv.Rules{ "Name": {myVerifier.NotEmpty(), myVerifier.Lt("10")}, "Age": {myVerifier.Lt("100")}, } //map[Age:[lt#100] Name:[notEmpty lt#10]] ``` 规则含义为`Age`字段长度或值小于100,`Name`字段不为空且长度或值小于10。 ### 验证器 先通过反射获取待检验参数的值和类型,判断是否为`struct`(目前只实现了对`struct`校验的功能,计划后续加入对`map`的校验功能), 获取`struct`属性数量并遍历所有属性,并遍历每个字段下所有规则,对定义的每一个规则进行校验是否合格。 ```go func (verifier verifier) Verify(st interface{}, rules Rules) (err error) { typ := reflect.TypeOf(st) val := reflect.ValueOf(st) if val.Kind() != reflect.Struct { return errors.New("expect struct") } num := val.NumField() //遍历需要验证对象的所有字段 for i := 0; i < num; i++ { tagVal := typ.Field(i) val := val.Field(i) if len(rules[tagVal.Name]) > 0 { for _, v := range rules[tagVal.Name] { switch { case v == "notEmpty": if isEmpty(val) { return errors.New(tagVal.Name + " value can not be nil") } case verifier.conditions[strings.Split(v, verifier.separator)[0]]: if !compareVerify(val, v, verifier.separator) { return errors.New(tagVal.Name + " length or value is illegal," + v) } } } } } return nil } ``` 规则校验有两种,分别是判空 和条件校验。 判空是通过反射`reflect.Value`获得字段值,并通过反射`value.Kind()`获得字段类型。 最终使用`switch`分别对不同类型 字段进行判断。 ```go func isEmpty(value reflect.Value) bool { switch value.Kind() { case reflect.String: return value.Len() == 0 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return value.Int() == 0 //此处省略其他类型判断 //... } return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) } ``` 条件校验则是通过开始时定义的范围条件进行校验,传入反射`reflect.Value`获得字段值,定义的规则,以及规则中的分隔符。先通过`switch`判断其类型, 再通过`switch`判断条件是大于小于或是其他条件,然后进行相应判断。 ```go func compareVerify(value reflect.Value, verifyStr, separator string) bool { switch value.Kind() { case reflect.String, reflect.Slice, reflect.Array: return compare(value.Len(), verifyStr, separator) //此处省略其他类型判断 //... default: return false } } ``` ### 封装 为了调用方便,做了一层封装,使用[函数选项模式](https://ormissia.github.io/posts/knowledge/2021-07-22/)对校验器进行封装,使调用更为方便。 ```go var defaultVerifierOptions = verifierOptions{ separator: ":", conditions: map[string]bool{ eq: true, ne: true, gt: true, lt: true, ge: true, le: true, }, } type VerifierOption func(o *verifierOptions) type verifierOptions struct { conditions map[string]bool separator string } // SetSeparator Default separator is ":". func SetSeparator(seq string) VerifierOption { return func(o *verifierOptions) { o.separator = seq } } func SwitchEq(sw bool) VerifierOption { return func(o *verifierOptions) { o.conditions[eq] = sw } } //... //此处省略其他参数的设置 type Verifier interface { Verify(obj interface{}, rules Rules) (err error) NotEmpty() string Ne(limit string) string Gt(limit string) string Lt(limit string) string Ge(limit string) string Le(limit string) string } type verifier struct { separator string conditions map[string]bool } func NewVerifier(opts ...VerifierOption) Verifier { options := defaultVerifierOptions for _, opt := range opts { opt(&options) } return verifier{ separator: options.separator, conditions: options.conditions, } } //... //此处省略接口的实现 ``` ### 发布 > 好了,基本功能完成了,如果仅仅是放在每个项目的`utils`拷来拷去,显然十分的不优雅。 > 那么这就需要发布到[pkg.go.dev](https://pkg.go.dev)才能通过`go get`命令正常被其他项目所引用。 1. 首先是`git commit`、`git push`一把梭将项目整到`GitHub`上。 2. 由于[pkg.go.dev](https://pkg.go.dev)的版本管理机制需要给项目打上`tag`,`git tag v0.0.1`基础版本,😋先定个`0.0.1`吧, 然后`git push`再走一遍。 3. 当然这时候还没完,需要自己`go get`一下,加上`GitHub`仓库名执行一下`go get github.com/ormissia/go-opv` 4. 这样仓库就可以正常被引用了。而且用不了多久,就可以从[pkg.go.dev](https://pkg.go.dev)上搜到相应的项目了。 5. 最后贴一下次项目的连接:[go-opv](https://pkg.go.dev/github.com/ormissia/go-opv) > 当然,这个过程中也遇到过小坑。项目中`go.mod`中的模块名需要写`GitHub`的仓库地址,对应此项目即为`module github.com/ormissia/go-opv`。 > 如果项目版本有更新,打了新的`tag`之后。可以通过`go get github.com/ormissia/go-opv@v0.0.3`拉取指定版本,目前尚不清楚 > [pkg.go.dev](https://pkg.go.dev)是否会自动同步`GitHub`上最新的`tag`。 ## 检验 测试用例? 好吧,`// TODO` > 老铁看到底了,来个star吧😁 > ↓↓↓↓↓↓↓↓↓ > [GitHub仓库](https://github.com/ormissia/go-opv)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1363 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传