V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
faceair
V2EX  ›  分享创造

分享一个 Golang 参数校验框架 jio

  •  2
     
  •   faceair ·
    faceair · 2018-12-02 14:29:07 +08:00 · 6850 次点击
    这是一个创建于 2181 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在写 Golang 的 WebServer 时我就很难受,缺一个类似 Node.js 的 joi 的框架,有很多想写的规则写不出来。

    在 Golang 社区中参数校验一般是用 https://github.com/go-playground/validator 或者 https://github.com/astaxie/beego/tree/develop/validation 这些类似的东西。

    我在使用这类框架的时候遇到了两个问题:

    1. struct 的初始零值会干扰校验。比如 struct 上有个字段是 int,你在校验规则里写 required 的意思是期望客户端必须要传值上来,但是如果客户端传 0 上来校验就会失败,因为框架没法区分这个零值是客户端传上来的还是客户没写时反序列化默认赋的值。
    2. 拓展规则的语法难用。往往你要再很远的地方往 validator 对象上 register 一个 type,然后再在你的 struct 去用。如果这类规则是一次性的话,跟自己的代码逻辑离得的太远了可读性大大降低。

    先给个 jio 的使用例子

    package main
    
    import (
        "errors"
        "io/ioutil"
        "net/http"
    
        "github.com/faceair/jio"
        "github.com/go-chi/chi"
    )
    
    func main() {
        r := chi.NewRouter()
        r.Route("/people", func(r chi.Router) {
            r.With(jio.ValidateBody(jio.Object().Keys(jio.K{
                "name": jio.String().Transform(func(ctx *jio.Context) {
                    if ctx.Value != "faceair" {
                        ctx.Abort(errors.New("you are not faceair"))
                    }
                }).Required(),
                "age":   jio.Number().Integer().Min(0).Max(100).Required(),
                "phone": jio.String().Regex(`^1[34578]\d{9}$`).Required(),
            }), jio.DefaultErrorHandler)).Post("/", func(w http.ResponseWriter, r *http.Request) {
                body, err := ioutil.ReadAll(r.Body)
                if err != nil {
                    panic(err)
                }
                w.Header().Set("Content-Type", "application/json; charset=utf-8")
                w.WriteHeader( http.StatusOK)
                w.Write(body)
            })
        })
        http.ListenAndServe(":8080", r)
    }
    

    我是这么解决这些问题的。

    1. 既然 struct 缺少字段是否存在的信息,我们就不把 struct 的反序列化跟参数校验搞到一起。在反序列化之前对数据做校验,我选择把这个过程放到一个中间件里,让校验无法通过代码就无需再往下走。
    2. 支持在定义 schema 的时候直接拓展规则,上文中 name 这个字段我就直接拓展了一个规则。

    同时 jio 还能给参数设置默认值、帮你做类型转换(string 转 array 转 int 等等)、能选择校验规则的顺序、能根据其他字段的数据选择不同的校验规则等等,我觉得 jio 灵活性比任何一个校验框架都要好(也包括 joi)。

    更多的使用文档可以直接查看 https://github.com/faceair/jio/blob/master/README.zh.md 欢迎大家多多使用,可以给 star 鼓励,也可以给与一些反馈我好继续迭代改进。

    14 条回复    2019-07-12 15:25:20 +08:00
    Cbdy
        1
    Cbdy  
       2018-12-02 15:37:15 +08:00 via Android
    我写过一个 joi 风格的 Java 的,可以交流一下
    https://github.com/cbdyzj/joi
    但实际实践下来,参数校验最佳实践还是 jsr303
    blless
        2
    blless  
       2018-12-02 17:22:54 +08:00 via Android
    为啥不用 tag 啊
    bubuhere
        3
    bubuhere  
       2018-12-02 17:41:40 +08:00 via iPhone   ❤️ 1
    已 Star
    faceair
        4
    faceair  
    OP
       2018-12-02 17:42:30 +08:00
    @blless #2 我在描述中不是说了吗?我觉得 tag 一点都不好用。
    zn
        5
    zn  
       2018-12-02 18:31:42 +08:00 via iPhone
    这语法太啰嗦了,无法想象。
    blless
        6
    blless  
       2018-12-02 18:34:03 +08:00 via Android
    想了想我自己也写过一个,不过还是用 tag,不过我没有 required 这个关键字,只有一个 default,没有 default 就是 required,序列化没有直接传入对象指针 encode,用 fastjson 直接判断是否存在字段…
    所以 1 不是问题,2 如果你的框架很多地方要检验同一个扩展规则,也是要另外放一个地方写的。所以这些问题我觉得都不是 tag 问题,而且目前参数检验框架的问题
    loading
        7
    loading  
       2018-12-02 18:46:04 +08:00 via Android
    为啥这个 go 代码看起来这么恶心!!
    faceair
        8
    faceair  
    OP
       2018-12-02 19:03:43 +08:00
    @blless #6 用 tag 可以解决 90% 的校验的问题吧。我可能是因为之前用过 joi 脑子里有些执念,另外还有一些骚操作想实现就自己重新造了一套。理性的说这些功能也不一定是人人都需要,比如:
    1. 类型转换
    字符串转布尔

    字符串转数字

    2. 根据其他字段选择校验规则

    3. 业务层的逻辑校验用 Transform 也写进 jio 里,后面的业务逻辑里不用再关心数据对不对

    🤔不过这些理论上有一部分也是可以自己写 parser 用 tag 实现,就是看到时候够不够像这么直观了。
    只是一个轮子罢了,我自己可能有一些想法和思考,如果能碰上臭味相投的人就更好了 🤓
    faceair
        9
    faceair  
    OP
       2018-12-02 19:06:48 +08:00
    只是记得回复里不能用 markdown 贴代码,不过贴图上来以后这图片的大小处理也好恶心... 各位李姐万岁...
    faceair
        10
    faceair  
    OP
       2018-12-02 19:13:29 +08:00
    @loading #7
    @zn #5

    https://gist.github.com/faceair/58c60e768edb464ad550cc7f39c2950f

    这么写会好受一点吗?剩下的要是觉得 schema 的定义也很啰嗦的话,那我这里也没有更好的办法了,设计就是这样的了...
    reus
        11
    reus  
       2018-12-02 21:50:18 +08:00
    感觉太罗嗦了
    Mitt
        12
    Mitt  
       2018-12-04 01:19:11 +08:00 via iPhone
    啰嗦但是有用,tag 是真的很难用,我一直想不通为什么要这么设计
    layxy
        13
    layxy  
       2019-07-12 10:20:02 +08:00
    go 上面没有一个好用的参数校验组件,都不能检查是 go 基本类型默认值还是调用者传的参数,有默认值的基本类型就没办法向调用者返回正确的错误信息,比如 A 调用 B 参数有一个布尔类型,该参数非必填,A 没有传该参数,B 接受反序列化结构体的时候该布尔类型会默认为 false,导致交互上的误导和错误,而且目前的校验框架是这样的,如果我传的参数和默认值一致也会认为我没传该参数,奇葩的逻辑和处理方式
    faceair
        14
    faceair  
    OP
       2019-07-12 15:25:20 +08:00
    @layxy jio 够使么?还是有别的场景用不了 jio ?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2730 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 15:25 · PVG 23:25 · LAX 07:25 · JFK 10:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.