Golang-03 自定义validator,实现java注解功能

0x00 About

接口开发中, 比较常用的操作就是对输入的参数Bean进行字段属性值校验.
Java中, 有Annotation(注解)可以让我们方便的在的类上面添加校验信息,
那么在Go中应该如何做到这一点呢?

0x01 structTag

我们先来看一下strcut的语法定义:https://golang.org/ref/spec#Struct_types


StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .</pre>


也就是说, struct字段的后面, 可以添加一个字符串, 称之为Tag

然后, 通过go语言中的reflect反射机制, 就可以读取相应字段的Tag信息了.
接下来, 找一段代码直观感受一下:

type InParam struct {
    StudentName string `json:"name" cc:"str,min=5,max=15"`
    Score       int    `json:"score" cc:"num,min=0,max=100"`
}

package main

import (
    "fmt"
    "reflect"
)
func main() {
    t := reflect.TypeOf(&InParam{"18", 25})
    field := t.Elem().Field(0)

    jsonName := field.Tag.Get("json")
    cc := field.Tag.Get("cc")

    fmt.Printf("%s(%s): %s", field.Name, jsonName, cc)
}

输出结果:
StudentName(name): str,min=5,max=15

需要注意的地方: reflect.TypeOf 的参数只能接入对象.

0x02 Validator

那么接下来的工作, 就是根据Tag内容开发一个公共函数.
这里提供了两种验证器:stringnumber, 分别验证其长度范围和取值范围.

package main

import (
    "fmt"
    "reflect"
    "strings"
)

// 定义接口
type CCValid interface {
    Validate(interface{}) (bool, error)
}

// 定义三个验证器
type CCDefaultValid struct {
}
type CCNumberValid struct {
    Min int
    Max int
}
type CCStringValid struct {
    Min int
    Max int
}

// 三个验证器实现接口方法
func (c CCNumberValid) Validate(obj interface{}) (bool, error) {
    v := obj.(int)
    if v < c.Min || v > c.Max {
        return false, fmt.Errorf(":int value should in range (%d, %d)", c.Min, c.Max)
    }
    return true, nil
}
func (c CCStringValid) Validate(obj interface{}) (bool, error) {
    l := len(obj.(string))
    if l < c.Min || l > c.Max {
        return false, fmt.Errorf(":string length should in range (%d, %d)", c.Min, c.Max)
    }
    return true, nil
}

func (c CCDefaultValid) Validate(obj interface{}) (bool, error) {
    return true, nil
}

var tagName = "cc"

//  公共方法,对外提供检验处理
func CCValidate(s interface{}) []error {
    var errs []error
    v := reflect.ValueOf(s)

    for i := 0; i < v.NumField(); i++ {
        tag := v.Type().Field(i).Tag.Get(tagName)
        if tag == "" || tag == "-" {
            continue
        }

        validator := parseValidatorFromTag(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
}

// 从Tag字符串里分析出使用哪个验证器,并赋值
func parseValidatorFromTag(tag string) CCValid {
    args := strings.Split(tag, ",")
    switch args[0] {
    case "num":
        v := CCNumberValid{}
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &v.Min, &v.Max)
        return v
    case "str":
        v := CCStringValid{}
        fmt.Sscanf(strings.Join(args[1:], ","), "min=%d,max=%d", &v.Min, &v.Max)
        return v
    }
    return CCDefaultValid{}
}

0x03 在gin中的使用

我们开发校验器的目的, 当然, 是为了能在gin中使用啦
直接上代码, 理解起来不难:


type Result struct {
    code int
    msg  string
    data interface{}
}

func OK(msg string) Result {
    return Result{0, msg, nil}
}

func NG(msg string) Result {
    return Result{1, msg, nil}
}


    // 解析JSON请求
    r.POST("/login", func(c *gin.Context) {
        var header MyHeader
        var param Login

        // 解析Header
        if c.BindHeader(&header) != nil {
            c.JSON(200, NG("invalid header"))
            return
        }

        // 解析JSON
        if c.BindJSON(&param) != nil {
            c.JSON(200, NG("invalid json param"))
            return
        }

        // 验证
        for _, firstErr := range CCValidate(param) {
            c.JSON(200, NG(firstErr.Error()))
            return
        }

        c.JSON(200, gin.H{"hello": param.Username, "world": param.Password, "from": header.From})
    })

当验证出错时, 会向客户端返回第一个发现的错误.

0x04 TODO

现在的代码来看, 业务功能还没开发呢, 已经写了这么多行代码了. 一点都不优雅.
接下来要考虑, 能不能利用中间件, 完成这些前置操作.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容