GO学习笔记 - 数据校验

本文主题:基于asaskevich/govalidator实现Golang数据校验

小慢哥的原创文章,欢迎转载


目录

▪ 一. asaskevich/govalidator介绍
▪ 二. 字符串匹配
▪ 三. struct元素匹配
▪ 四. struct元素可选验证
▪ 五. struct嵌套校验
▪ 六. 无法实现嵌套的可选校验
▪ 七. 个人最佳实践
▪ 八. 其他功能
▪ 附录1. 字符串合法性校验
▪ 附录2. struct元素校验项
▪ 附录3. 数据特征匹配
▪ 附录4. 类型转换
▪ 附录5. 裁剪、处理、填充、遍历等


一. asaskevich/govalidator介绍

godoc里可以搜到若干相似的第三方数据校验模块,但笔者推荐使用asaskevich/govalidator,原因:

▷ star最多、持续更新发布
▷ 功能完善、使用便利
▷ 丰富的字符串校验、数据匹配、裁剪拼接处理等
▷ 支持struct元素合法性校验,并且支持嵌套检查
▷ 源码值得学习,就是一个百宝箱

// 下载
go get github.com/asaskevich/govalidator

注意:查看使用方法到github,查看支持的函数列表到godoc

https://github.com/asaskevich/govalidator
https://godoc.org/github.com/asaskevich/govalidator

二. 字符串匹配

govalidator支持非常多种字符串匹配,先贴上一个简单例子

package main

import (
    "fmt"
    "github.com/asaskevich/govalidator"
)

func main() {
    // 判断字符串值是否为合法的IPv4地址
    ip4 := "192.168.1.1"
    fmt.Println(govalidator.IsIPv4(ip4)) // true

    // 判断字符串值是否为合法的MAC
    mac := "aa:bb:cc:dd:ee:ffffff"
    fmt.Println(govalidator.IsMAC(mac)) // false

    // 判断数字是否在指定范围内
    dig := 101    // string类型也可以用
    fmt.Println(govalidator.InRange(dig, 0, 100)) // false
}

输出

true
false
false

完整的可用校验方法列表详见本文附录1、3


三. struct元素匹配

govalidator专门提供了一个函数,用于校验struct的元素

govalidator.ValidateStruct()

简单例子

package main

import (
    "fmt"
    "github.com/asaskevich/govalidator"
)

type foo struct {
    A string `valid:"ipv4"`
    B string `valid:"mac"`
    C string `valid:"range(0|100)"`    // 也可以使用int类型
}

func main() {
    f := foo{
        A: "192.168.1.1",
        B: "aa:bb:cc:dd:ee:ffffff",
        C: "101",
    }

    result, err := govalidator.ValidateStruct(f)
    if err != nil {
        fmt.Println("error: " + err.Error())
    }
    fmt.Println(result)
}

输出

error: B: aa:bb:cc:dd:ee:ffffff does not validate as mac;C: 101 does not validate as range(0|100)
false

注意:

▪ struct元素只支持部分常用的校验,详见本文附录2
▪ struct元素必须是导出型,也就是必须大写字母开头,govalidator才会去理会
▪ struct元素匹配较为智能,比如range(min|max)不仅支持string也支持int类型


四. struct元素可选验证

govalidator有一个bool类型的全局变量,可通过函数govalidator.SetFieldsRequiredByDefault()进行设置:

▷ 当设置为true时,如果没有定义valid tag,则会提示错误
▷ 当设置为false时,如果没有定义valid tag,不会提示错误。默认值就是false

另外,valid tag里,可以通过显式设置方式更细颗粒度地控制:当遇到zero value时是需要验证还是提示错误。此设置可以覆盖SetFieldsRequiredByDefault()。所以,valid tag有如下几种写法

`valid:""` // 等同于空tag,即``
`valid:"-"`
`valid:","`
`valid:",optional`
`valid:",required`

接下来,分别测试:假设一个struct元素的值为空字符""(即zero value)

▷ govalidator.SetFieldsRequiredByDefault(true)

`valid:""`    // 报错:All fields are required to at least have one validation defined
`valid:"-"`    // true
`valid:","`    // 报错:Missing required field
`valid:",optional`    // true
`valid:",required`    // 报错:non zero value required
`valid:"ipv4"`    // 报错:Missing required field
`valid:"ipv4,optional"`    // true
`valid:"ipv4,required"`    // 报错:non zero value required

▷ govalidator.SetFieldsRequiredByDefault(false)

`valid:""`    // true
`valid:"-"`    // true
`valid:","`    // true
`valid:",optional`    // true
`valid:",required`    // non zero value required
`valid:"ipv4"`    // true
`valid:"ipv4,optional"`    // true
`valid:"ipv4,required"`    // 报错:non zero value required

继续测试,当struct元素的值为不合法的ipv4地址字符串(非空字符串),如"192.168.1.1.1"

▷ govalidator.SetFieldsRequiredByDefault(true)

`valid:""`    // 报错:All fields are required to at least have one validation defined
`valid:"-"`    // true
`valid:","`    // true
`valid:",optional`    // true
`valid:",required`    // true
`valid:"ipv4"`    // 报错:192.168.1.1.1 does not validate as ipv4
`valid:"ipv4,optional"`    // 报错:192.168.1.1.1 does not validate as ipv4
`valid:"ipv4,required"`    // 报错:192.168.1.1.1 does not validate as ipv4

▷ govalidator.SetFieldsRequiredByDefault(false):测试效果和上述完全相同

另外,还有一个全局变量参数,通过govalidator.SetNilPtrAllowedByRequired()设置,但由于笔者尚未测试过,因此直接贴出官方解释

// 来自github
SetNilPtrAllowedByRequired causes validation to pass when struct fields marked by required are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between nil and zero value state can use this. If disabled, both nil and zero values cause validation errors.

// 来自godoc
SetNilPtrAllowedByRequired causes validation to pass for nil ptrs when a field is set to required. The validation will still reject ptr fields in their zero value state. Example with this enabled:

type exampleStruct struct {
    Name *string `valid:"required"

With `Name` set to "", this will be considered invalid input and will cause a validation error. With `Name` set to nil, this will be considered valid by validation. By default this is disabled.

五. struct嵌套校验

嵌套元素名必须是导出型,也就是大写字母开头,举例

package main

import (
    "fmt"
    "github.com/asaskevich/govalidator"
)

type Foo struct {
    A string `valid:"ipv4"`
    B string `valid:"mac"`
    C int `valid:"range(0|100)"`
}

type bar struct {
    X string `valid:"ipv4"`
    Foo `valid:",required"`
}

func main() {
    govalidator.SetFieldsRequiredByDefault(true)

    b := bar{
        X: "192.168.1.1",
    }

    b.Foo.A = "192.168.1.1.1"
    b.Foo.B = "aa:bb:cc:dd:ee:ff"
    b.Foo.C = 100

    result, err := govalidator.ValidateStruct(b)
    if err != nil {
        fmt.Println("error: " + err.Error())
    }
    fmt.Println(result)
}

输出

error: Foo.A: 192.168.1.1.1 does not validate as ipv4;A: 192.168.1.1.1 does not validate as ipv4
false

注意:可以给Foo设置一个元素名,但也必须是大写字母开头,比如

MyFoo Foo `valid:",required"`    // 正确,可以读取到
myFoo Foo `valid:",required"`    // 错误,无法读取到

六. 无法实现嵌套的可选校验

无法实现以嵌套为颗粒度的可选校验,比如下面这样是没有效果的

type bar struct {
    X string `valid:"ipv4"`
    Foo `valid:",optional"`    // 不可行
}

因为上面代码实际会被转换为这样

type bar struct {
    X string `valid:"ipv4"`
    Foo.A string `valid:"ipv4"`
    Foo.B string `valid:"mac"`
    Foo.C int `valid:"range(0|100)"`
}

这就导致没有办法实现Foo全校验或者全不校验


七. 个人最佳实践

建议全部显式配置校验,因为使用隐式一旦配置有误,难以及时发现

▷ govalidator.SetFieldsRequiredByDefault(true)
▷ valid tag写法:带上required,例如:

想做验证使用`valid:ipv4,required`
不想做验证使用`valid:",required"`

八. 其他功能

govalidator的校验功能还支持自定义tag与自定义校验函数,由于笔者尚未深度实践过,因此请参考官方github文档。

govalidator除了支持校验,还支持较为丰富的字符串裁剪、处理、正则等功能,以及若干类型转换功能,详见本文附录4、5(本文相比godoc和官网文档进行了更为细致的分类)。但笔者不推荐直接使用这些裁剪、处理、正则功能,因为实际上就是做了一层封装和一些细节处理,并不复杂,但可以学习。

笔者认为在使用govalidator的任何功能时,先看看源码,这是一个大而全的源码宝库,非常值得学习和借鉴。


附录1. 字符串合法性校验

下面都是业务级别的合法校验,比如是否为IPv4格式,是否为URL

func IsBase64(str string) bool
func IsCIDR(str string) bool    // 是否为合法的CIDR格式,包含了IPv4与IPv6
func IsCreditCard(str string) bool
func IsDNSName(str string) bool
func IsDataURI(str string) bool
func IsEmail(str string) bool
func IsExistingEmail(email string) bool
func IsFilePath(str string) (bool, int)
func IsHash(str string, algorithm string) bool
func IsHexcolor(str string) bool
func IsHost(str string) bool
func IsIP(str string) bool    // 是否为合法的IP地址,包含了IPv4与IPv6
func IsIPv4(str string) bool
func IsIPv6(str string) bool
func IsISBN(str string, version int) bool
func IsISBN10(str string) bool
func IsISBN13(str string) bool
func IsISO3166Alpha2(str string) bool
func IsISO3166Alpha3(str string) bool
func IsISO4217(str string) bool
func IsISO693Alpha2(str string) bool
func IsISO693Alpha3b(str string) bool
func IsJSON(str string) bool    // 通过json.Unmarshal()是否返回error进行判断
func IsLatitude(str string) bool
func IsLongitude(str string) bool
func IsMAC(str string) bool    // 支持aa:bb:cc:dd:ee:ff,以及aabb.ccdd.eeff格式
func IsMongoID(str string) bool
func IsPort(str string) bool
func IsRFC3339(str string) bool
func IsRFC3339WithoutZone(str string) bool
func IsRGBcolor(str string) bool
func IsRequestURI(rawurl string) bool
func IsRequestURL(rawurl string) bool
func IsRsaPub(str string, params ...string) bool
func IsRsaPublicKey(str string, keylen int) bool
func IsSSN(str string) bool
func IsSemver(str string) bool
func IsTime(str string, format string) bool
func IsURL(str string) bool
func IsUUID(str string) bool    // 包含UUIDv3、UUIDv4、UUIDv5
func IsUUIDv3(str string) bool
func IsUUIDv4(str string) bool
func IsUUIDv5(str string) bool

附录2. struct元素校验项

有2种,第一种是不带参数的,第二种是带参数的

▷ 第一种:不带参数(第一列表示在valid tag里怎么写,第二列表示相当于govalidator的哪个导出函数)

"email": IsEmail,
"url": IsURL,
"dialstring": IsDialString,
"requrl": IsRequestURL,
"requri": IsRequestURI,
"alpha": IsAlpha,
"utfletter": IsUTFLetter,
"alphanum": IsAlphanumeric,
"utfletternum": IsUTFLetterNumeric,
"numeric": IsNumeric,
"utfnumeric": IsUTFNumeric,
"utfdigit": IsUTFDigit,
"hexadecimal": IsHexadecimal,
"hexcolor": IsHexcolor,
"rgbcolor": IsRGBcolor,
"lowercase": IsLowerCase,
"uppercase": IsUpperCase,
"int": IsInt,
"float": IsFloat,
"null": IsNull,
"uuid": IsUUID,
"uuidv3": IsUUIDv3,
"uuidv4": IsUUIDv4,
"uuidv5": IsUUIDv5,
"creditcard": IsCreditCard,
"isbn10": IsISBN10,
"isbn13": IsISBN13,
"json": IsJSON,
"multibyte": IsMultibyte,
"ascii": IsASCII,
"printableascii": IsPrintableASCII,
"fullwidth": IsFullWidth,
"halfwidth": IsHalfWidth,
"variablewidth": IsVariableWidth,
"base64": IsBase64,
"datauri": IsDataURI,
"ip": IsIP,
"port": IsPort,
"ipv4": IsIPv4,
"ipv6": IsIPv6,
"dns": IsDNSName,
"host": IsHost,
"mac": IsMAC,
"latitude": IsLatitude,
"longitude": IsLongitude,
"ssn": IsSSN,
"semver": IsSemver,
"rfc3339": IsRFC3339,
"rfc3339WithoutZone": IsRFC3339WithoutZone,
"ISO3166Alpha2": IsISO3166Alpha2,
"ISO3166Alpha3": IsISO3166Alpha3,

▷ 第二种:带参数(第一列表示在valid tag里怎么写,第二列表示相当于govalidator的哪个导出函数)

"range(min|max)": Range,
"length(min|max)": ByteLength,
"runelength(min|max)": RuneLength,
"stringlength(min|max)": StringLength,
"matches(pattern)": StringMatches,
"in(string1|string2|...|stringN)": IsIn,
"rsapub(keylength)" : IsRsaPub,

附录3. 数据特征匹配

下面是非业务的数据校验,比如在一个字符串中是否包含固定字符、是否包含空白符、是否正整数

func ByteLength(str string, params ...string) bool
func Contains(str, substring string) bool
func HasLowerCase(str string) bool
func HasUpperCase(str string) bool
func HasWhitespace(str string) bool
func HasWhitespaceOnly(str string) bool
func InRange(value interface{}, left interface{}, right interface{}) bool
func InRangeFloat32(value, left, right float32) bool
func InRangeFloat64(value, left, right float64) bool
func InRangeInt(value, left, right interface{}) bool
func IsASCII(str string) bool
func IsAlpha(str string) bool
func IsAlphanumeric(str string) bool
func IsByteLength(str string, min, max int) bool
func IsDialString(str string) bool
func IsDivisibleBy(str, num string) bool
func IsFloat(str string) bool
func IsFullWidth(str string) bool
func IsHalfWidth(str string) bool
func IsHexadecimal(str string) bool
func IsIn(str string, params ...string) bool
func IsInt(str string) bool
func IsLowerCase(str string) bool
func IsMultibyte(str string) bool
func IsNatural(value float64) bool
func IsNegative(value float64) bool
func IsNonNegative(value float64) bool // >=0
func IsNonPositive(value float64) bool // <=0
func IsNull(str string) bool // 空字符串
func IsNumeric(str string) bool // 字符串里仅包含数字
func IsPositive(value float64) bool // 正数
func IsPrintableASCII(str string) bool
func IsUTFDigit(str string) bool
func IsUTFLetter(str string) bool
func IsUTFLetterNumeric(str string) bool
func IsUTFNumeric(str string) bool
func IsUpperCase(str string) bool
func IsVariableWidth(str string) bool
func IsWhole(value float64) bool // 整数
func Matches(str, pattern string) bool // 正则匹配
func Range(str string, params ...string) bool // 字符串长度,params的string会转换成float64然后调用InRange(),主要是用于struct tag的range(min|max)
func RuneLength(str string, params ...string) bool // alias for StringLength
func Sign(value float64) float64 // 如果大于0则返回1,等于0返回0,小于0返回-1
func StringLength(str string, params ...string) bool // 字符串长度在指定范围内(视为utf8)
func StringMatches(s string, params ...string) bool // 正则匹配,等同于Matches(),主要是用于struct tag的range(min|max)

附录4. 类型转换

func ToBoolean(str string) (bool, error)
func ToFloat(str string) (float64, error)
func ToInt(value interface{}) (res int64, err error)
func ToString(obj interface{}) string
func ToJSON(obj interface{}) (string, error)
func NormalizeEmail(str string) (string, error)    // 输出规范化的电子邮件格式

附录5. 裁剪、处理、填充、遍历等

func Abs(value float64) float64 // 获得绝对值
func BlackList(str, chars string) string // 从字符串中移除指定字符
func CamelCaseToUnderscore(str string) string // 将驼峰拼写法转换为下划线分割写法,如MyFunc => my_func
func Count(array []interface{}, iterator ConditionIterator) int // 通过自定义ConditionIterator(实现了迭代器)来实现判断count
func Each(array []interface{}, iterator Iterator) // 通过自定义Iterator(实现了迭代器)来实现操作,不做任何返回,自行处理,比如打印一些东西
func Filter(array []interface{}, iterator ConditionIterator) []interface{} // 通过自定义ConditionIterator(实现了迭代器)来对[]interface{}的元素进行遍历处理
func Find(array []interface{}, iterator ConditionIterator) interface{} // 通过自定义ConditionIterator(实现了迭代器)来对[]interface{}的元素进行遍历查找,返回第一个找到的,若都没找到则返回nil
func GetLine(s string, index int) (string, error) // 从含多行的字符串中返回指定行内容(0为第一行),res, err := valid.GetLine("aa\nbb\ncc\n", 1) 返回bb
func GetLines(s string) []string // 将字符串的换行符去掉,返回由每一行组成的slice,原理就是strings.Split(s, "\n")
func LeftTrim(str, chars string) string // 若字符串最左边匹配了chars,则删除,如果chars为"",则删除前导符(空格、tab、换行符)
func Map(array []interface{}, iterator ResultIterator) []interface{}
func PadBoth(str string, padStr string, padLen int) string // 字符串首尾填充字符
func PadLeft(str string, padStr string, padLen int) string // 字符串开头填充字符
func PadRight(str string, padStr string, padLen int) string // 字符串末尾填充字符
func RemoveTags(s string) string // RemoveTags remove all tags from HTML string
func ReplacePattern(str, pattern, replace string) string // 将正则匹配到的字符用指定字符替换
func Reverse(s string) string // 字符反转
func RightTrim(str, chars string) string // 若字符串最右边匹配了chars,则删除,如果chars为"",则删除前导符(空格、tab、换行符)
func SafeFileName(str string) string // 返回安全的文件名,裁剪掉空格等符号,转小写字母等
func Trim(str, chars string) string // 就是LeftTrim+RightTrim
func Truncate(str string, length int, ending string) string
func UnderscoreToCamelCase(s string) string // 将下划线分割写法转换为驼峰拼写法
func WhiteList(str, chars string) string // 从字符串中移除非指定字符