![](/style/images/good.png)
![](/style/images/bad.png)
趁周末写了个小工具 - Golang 实体参数校验器
source link: https://studygolang.com/articles/35174
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
趁周末写了个小工具 - Golang 实体参数校验器
ormissia · 2天之前 · 259 次点击 · 预计阅读时间 7 分钟 · 大约8小时之前 开始浏览A:"请用一句话让别人知道你写过Golang。"
B:"if err!= nil ..."
只要是接触过Golang的人,无不为其if err != nil
的语法感到惊奇,或是大加赞赏,或是狠狠痛批。作为使用者,不管喜欢也好,反对也罢,
目前还是要接受这种错误处理模式。
而最令人头痛的就是请求参数中各种值的校验。比如Get
请求中接收分页参数时,需要将string
格式的参数转换成int
类型,再如时间类型的参数 转换,
诸如此类,等等等等。好家伙,一个接口写完if err != nil
的判断占了一多半的行数,看着实在不爽。
下面就是一个典型的例子,而且这个接口参数还不是特别多
func Export(c *gin.Context) {
//删除开头
//...
var param map[string]string
err := c.ShouldBindJSON(¶m)
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代码的时候,偶然看到一个通过反射去做参数校验的方式。 嘿,学到了!
校验规则使用一个map
存储,key
为字段名,value
为规则列表,并使用一个string
类型的切片来存储。
后续计划加入
tag
标签定义规则的功能以及增加通过函数参数的方式,实现自定义规则校验
type Rules map[string][]string
支持的规则有:
- 等于、不等于
- 大于、小于
- 大于等于、小于等于
对于数值类型为比较值大小,对于字符串或者切片等类型为比较长度大小
比如调用生成小于规则的方法,则会返回一个小于指定值规则的字符串,用于后面校验器使用
// Lt <
func (verifier verifier) Lt(limit string) string {
return fmt.Sprintf("%s%s%s", lt, verifier.separator, limit)
}
规则定义示例:
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
属性数量并遍历所有属性,并遍历每个字段下所有规则,对定义的每一个规则进行校验是否合格。
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
分别对不同类型 字段进行判断。
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
判断条件是大于小于或是其他条件,然后进行相应判断。
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
}
}
为了调用方便,做了一层封装,使用函数选项模式对校验器进行封装,使调用更为方便。
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才能通过go get
命令正常被其他项目所引用。
- 首先是
git commit
、git push
一把梭将项目整到GitHub
上。 - 由于pkg.go.dev的版本管理机制需要给项目打上
tag
,git tag v0.0.1
基础版本,😋先定个0.0.1
吧, 然后git push
再走一遍。 - 当然这时候还没完,需要自己
go get
一下,加上GitHub
仓库名执行一下go get github.com/ormissia/go-opv
- 这样仓库就可以正常被引用了。而且用不了多久,就可以从pkg.go.dev上搜到相应的项目了。
- 最后贴一下次项目的连接:go-opv
当然,这个过程中也遇到过小坑。项目中
go.mod
中的模块名需要写GitHub
的仓库地址,对应此项目即为module github.com/ormissia/go-opv
。 如果项目版本有更新,打了新的tag
之后。可以通过go get github.com/ormissia/[email protected]
拉取指定版本,目前尚不清楚 pkg.go.dev是否会自动同步GitHub
上最新的tag
。
测试用例?
好吧,// TODO
老铁看到底了,来个star吧😁
↓↓↓↓↓↓↓↓↓
GitHub仓库
有疑问加站长微信联系(非本文作者))
![280](https://static.studygolang.com/static/img/footer.png?imageView2/2/w/280)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK