golang 如何验证struct字段的数据格式
本文同时发表在https://github.com/zhangyachen/zhangyachen.github.io/issues/125
假设我们有如下结构体:
type User struct {
Id int
Name string
Bio string
Email string
}
我们需要对结构体内的字段进行验证合法性:
- Id的值在某一个范围内。
- Name的长度在某一个范围内。
- Email格式正确。
我们可能会这么写:
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}
if user.Id < 1 && user.Id > 1000 {
return false
}
if len(user.Name) < 2 && len(user.Name) > 10 {
return false
}
if !validateEmail(user.Email) {
return false
}
这样的话代码比较冗余,而且如果结构体新加字段,还需要再修改验证函数再加一段if判断。这样代码比较冗余。我们可以借助golang的structTag来解决上述的问题:
type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}
validate:"number,min=1,max=1000"就是structTag。如果对这个比较陌生的话,看看下面这个:
type User struct {
Id int `json:"id"`
Name string `json:"name"`
Bio string `json:"about,omitempty"`
Active bool `json:"active"`
Admin bool `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
写过golang的基本都用过json:xxx这个用法,json:xxx其实也是一个structTag,只不过这是golang帮你实现好特定用法的structTag。而validate:"number,min=1,max=1000"是我们自定义的structTag。
实现思路

我们定义一个接口Validator,定义一个方法Validate。再定义有具体意义的验证器例如StringValidator、NumberValidator、EmailValidator来实现接口Validator。
这里为什么要使用接口?假设我们不使用接口代码会怎么写?
if tagIsOfNumber(){
validator := NumberValidator{}
}else if tagIsOfString() {
validator := StringValidator{}
}else if tagIsOfEmail() {
validator := EmailValidator{}
}else if tagIsOfDefault() {
validator := DefaultValidator{}
}
这样的话判断逻辑不能写在一个函数中,因为返回值validator会因为structTag的不同而不同,而且validator也不能当做函数参数做传递。而我们定义一个接口,所有的validator都去实现这个接口,上述的问题就能解决,而且逻辑更加清晰和紧凑。
关于接口的使用可以看下标准库的io Writer,Writer是个interface,只有一个方法Writer:
type Writer interface {
Write(p []byte) (n int, err error)
}
而输出函数可以直接调用参数的Write方法即可,无需关心到底是写到文件还是写到标准输出:
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf) //调用Write方法
p.free()
return
}
//调用
Fprintf(os.Stdout, format, a...) //标准输出
Fprintf(os.Stderr, msg+"\n", args...) //标准错误输出
var buf bytes.Buffer
Fprintf(&buf, "[") //写入到Buffer的缓存中
言归正传,我们看下完整代码,代码是Custom struct field tags in Golang中给出的:
package main
import (
"fmt"
"reflect"
"regexp"
"strings"
)
const tagName = "validate"
//邮箱验证正则
var mailRe = regexp.MustCompile(`\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z`)
//验证接口
type Validator interface {
Validate(interface{}) (bool, error)
}
type DefaultValidator struct {
}
func (v DefaultValidator) Validate(val interface{}) (bool, error) {
return true, nil
}
type StringValidator struct {
Min int
Max int
}
func (v StringValidator) Validate(val interface{}) (bool, error) {
l := len(val.(string))
if l == 0 {
return false, fmt.Errorf("cannot be blank")
}
if l < v.Min {
return false, fmt.Errorf("should be at least %v chars long", v.Min)
}
if v.Max >= v.Min && l > v.Max {
return false, fmt.Errorf("should be less than %v chars long", v.Max)
}
return true, nil
}
type NumberValidator struct {
Min int
Max int
}
func (v NumberValidator) Validate(val interface{}) (bool, error) {
num := val.(int)
if num < v.Min {
return false, fmt.Errorf("should be greater than %v", v.Min)
}
if v.Max >= v.Min && num > v.Max {
return false, fmt.Errorf("should be less than %v", v.Max)
}
return true, nil
}
type EmailValidator struct {
}
func (v EmailValidator) Validate(val interface{}) (bool, error) {
if !mailRe.MatchString(val.(string)) {
return false, fmt.Errorf("is not a valid email address")
}
return true, nil
}
func getValidatorFromTag(tag string) Validator {
args := strings.Split(tag, ",")
switch args[0] {
case "number":
validator := NumberValidator{}
//将structTag中的min和max解析到结构体中
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "string":
validator := StringValidator{}
fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &validator.Min, &validator.Max)
return validator
case "email":
return EmailValidator{}
}
return DefaultValidator{}
}
func validateStruct(s interface{}) []error {
errs := []error{}
v := reflect.ValueOf(s)
for i := 0; i < v.NumField(); i++ {
//利用反射获取structTag
tag := v.Type().Field(i).Tag.Get(tagName)
if tag == "" || tag == "-" {
continue
}
validator := getValidatorFromTag(tag)
valid, err := validator.Validate(v.Field(i).Interface())
if !valid && err != nil {
errs = append(errs, fmt.Errorf("%s %s", v.Type().Field(i).Name, err.Error()))
}
}
return errs
}
type User struct {
Id int `validate:"number,min=1,max=1000"`
Name string `validate:"string,min=2,max=10"`
Bio string `validate:"string"`
Email string `validate:"email"`
}
func main() {
user := User{
Id: 0,
Name: "superlongstring",
Bio: "",
Email: "foobar",
}
fmt.Println("Errors:")
for i, err := range validateStruct(user) {
fmt.Printf("\t%d. %s\n", i+1, err.Error())
}
}
代码很好理解,结构也很清晰,不做过多解释了_
github上其实已经有现成的验证包了govalidator,支持内置支持的验证tag和自定义验证tag:
package main
import (
"github.com/asaskevich/govalidator"
"fmt"
"strings"
)
type Server struct {
ID string `valid:"uuid,required"`
Name string `valid:"machine_id"`
HostIP string `valid:"ip"`
MacAddress string `valid:"mac,required"`
WebAddress string `valid:"url"`
AdminEmail string `valid:"email"`
}
func main() {
server := &Server{
ID: "123e4567-e89b-12d3-a456-426655440000",
Name: "IX01",
HostIP: "127.0.0.1",
MacAddress: "01:23:45:67:89:ab",
WebAddress: "www.example.com",
AdminEmail: "admin@exmaple.com",
}
//自定义tag验证函数
govalidator.TagMap["machine_id"] = govalidator.Validator(func(str string) bool {
return strings.HasPrefix(str, "IX")
})
if ok, err := govalidator.ValidateStruct(server); err != nil {
panic(err)
} else {
fmt.Printf("OK: %v\n", ok)
}
}
参考资料:
golang 如何验证struct字段的数据格式的更多相关文章
- golang自定义struct字段标签
原文链接: https://sosedoff.com/2016/07/16/golang-struct-tags.html struct是golang中最常使用的变量类型之一,几乎每个地方都有使用,从 ...
- golang常用库:字段参数验证库-validator
背景 在平常开发中,特别是在web应用开发中,为了验证输入字段的合法性,都会做一些验证操作.比如对用户提交的表单字段进行验证,或者对请求的API接口字段进行验证,验证字段的合法性,保证输入字段值的安全 ...
- Golang面向对象编程-struct(结构体)
Golang面向对象编程-struct(结构体) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是面向对象编程 面向对象编程(Object Oriented Program ...
- .Net高级进阶,WebApi和MVC进行模型验证的时候,教你如何自由控制需要进行验证的字段?
现在,你有一个MVC架构的web项目,你要完成一个注册功能. 前台传了3个值到你的控制器,分别是账号.密码.邮箱. 如图:现在你要在控制器里面判断,账号名称.密码.邮箱不能为空,并且名称和密码不超过1 ...
- django 表单验证和字段验证
表单验证和字段验证 表单验证发生在数据验证之后.如果你需要自定义这个过程,有几个不同的地方可以修改,每个地方的目的不一样.表单处理过程中要运行三种类别的验证方法.它们通常在你调用表单的is_valid ...
- golang sqlx查询时, struct字段冲突
type TA struct { Id int64 `db:"id"` } type TB struct { Id int64 `db:"id"` } type ...
- GO开发[五]:golang结构体struct
Go结构体struct Go语言的结构体(struct)和其他语言的类(class)有同等的地位,但Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性 ...
- MVC ValidationAttribute 验证一个字段必须大于另一个字段
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] pu ...
- Golang结构体struct的使用(结构体嵌套, 匿名结构体等)
转自: https://studygolang.com/articles/11313 golang中是没有class的,但是有一个结构体struct,有点类似,他没有像java,c++中继承的概念,但 ...
随机推荐
- EntityFramework6与EntityFrameworkCore的区别
EntityFramework6 EF6 是一个久经考验的数据库访问技术,发展多年,拥有许多特性,并且成熟稳定.2008年EF作为 .Net 3.5 Sp1 和Visual Studio 2008 S ...
- Spark SQL中的几种join
1.小表对大表(broadcast join) 将小表的数据分发到每个节点上,供大表使用.executor存储小表的全部数据,一定程度上牺牲了空间,换取shuffle操作大量的耗时,这在SparkSQ ...
- 杭电ACM题单
杭电acm题目分类版本1 1002 简单的大数 1003 DP经典问题,最大连续子段和 1004 简单题 1005 找规律(循环点) 1006 感觉有点BT的题,我到现在还没过 1007 经典问题,最 ...
- C++反汇编第二讲,不同作用域下的构造和析构的识别
C++反汇编第二讲,不同作用域下的构造和析构的识别 目录大纲: 1.全局(静态)对象的识别,(全局静态全局一样的,都是编译期间检查,所以当做全局对象看即可.) 1.1 探究本质,理解构造和析构的生成, ...
- php获取当前月与上个月月初及月末时间戳的方法
php 获取今日.昨日.上周.本月的起始时间戳和结束时间戳的方法,主要使用到了 php 的时间函数 mktime.下面首先还是直奔主题以示例说明如何使用 mktime 获取今日.昨日.上周.本月的起始 ...
- Spring-MVC理解之一:应用上下文webApplicationContext
一.先说ServletContext javaee标准规定了,servlet容器需要在应用项目启动时,给应用项目初始化一个ServletContext作为公共环境容器存放公共信息.ServletCon ...
- netty源码分析
1.Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序.也就是说,Netty 是一个基于N ...
- 使用python
最近看视频学习,老师布置了个作业,关于如何使用python将多个excel进行合并,老师写的代码我感觉比较复杂,下面是我自己改良之后较简单的方式. 实现这个功能主要有两种方法,一种是用xlwd,xls ...
- Ubuntu 17.04 安装
不忘初心,方得始终. 今天听别人说17.04发布了,我是开心的不得了,赶紧下载了一个,准备安装在自己的本子上.这段时间已经花费了更多的时间在docker和OpenStack上面,没时间看新闻了,因此今 ...
- C++反汇编第四讲,反汇编中识别继承关系,父类,子类,成员对象
C++反汇编第四讲,反汇编中识别继承关系,父类,子类,成员对象 讲解目录: 1.各类在内存中的表现形式 备注: 主要复习开发知识,和反汇编没有关系,但是是理解反汇编的前提. 2.子类继承父 ...