bigywater.tech
Gin · 33

required 对零值的陷阱与指针

前端传了 {"age":0},后端却说 age 是必填?binding:"required" 对零值的判定常常出乎意料。这篇从 validator 的 hasValue 源码出发,用 JSON 和查询串把值类型、指针、切片、map、字符串、时间、嵌套结构体的“零值”与“没传”对比一遍。

4 / 4
值类型字段传 0 / "" / false / 0 时 required 失败的个数
0 / 4
指针字段传同样的零值时失败的个数
[] 通过
空切片 [] 与全空格字符串通过 required;不传才失败

required 看的是什么

实测 · 本机执行输出
  validator v10.30.1 baked_in.go 的 hasValue:
    1816 | func hasValue(fl FieldLevel) bool {
    1817 |     field := fl.Field()
    1818 |     switch field.Kind() {
    1819 |     case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
    1820 |         return !field.IsNil()
    1821 |     default:
    1822 |         if fl.(*validate).fldIsPointer && getValue(field) != nil {
    1823 |             return true
    1824 |         }
    1825 |         return field.IsValid() && !field.IsZero()
    1826 |     }
    1827 | }
    1828 | 
    1829 | // hasNotZeroValue is the validation function for validating if the current field's value is not the zero value for its type.

required 对切片、map、指针这类引用类型只看是不是 nil;对其它类型看是不是零值。所以 int 的 0、string 的空串、bool 的 false 都被当作“没提供”——这与 JSON 里“显式传了 0”是两回事。

指针区分“没传”和“零值”

实测 · 本机执行输出
    body={"age":18,"name":"a","admin":true,"score":1.5}
      值类型字段:通过
      指针字段 :通过
    body={"age":0,"name":"","admin":false,"score":0}
      值类型字段:不通过 V.Age(required) V.Name(required) V.Admin(required) V.Score(required)
      指针字段 :通过
    body={}
      值类型字段:不通过 V.Age(required) V.Name(required) V.Admin(required) V.Score(required)
      指针字段 :不通过 P.Age(required) P.Name(required) P.Admin(required) P.Score(required)
    body={"age":null,"name":null,"admin":null,"score":null}
      值类型字段:不通过 V.Age(required) V.Name(required) V.Admin(required) V.Score(required)
      指针字段 :不通过 P.Age(required) P.Name(required) P.Admin(required) P.Score(required)

要区分这两种情况,把字段改成指针:*int 配 required,传 0 时指针非 nil 通过,不传或 null 时是 nil 失败。代价是业务代码里要解引用。如果只想要“不能为空”,值类型的 required 恰好合适。

其它类型的细节

实测 · 本机执行输出
    全部给出且非零                    → 通过
    s:[] m:{} 空集合              → 通过
    s、m 不传                     → 不通过 T.S(required) T.M(required)
    str:"   "(三个空格)            → 通过
    at 为零时间 0001-01-01         → 不通过 T.At(required)
    at 不传                      → 不通过 T.At(required)
    in:{}(嵌套结构体字段全零)           → 不通过 T.In.X(required)
    pin 不传                     → 不通过 T.PIn(required)
    pin:{}                     → 不通过 T.PIn.X(required)

空数组 [] 和空对象 {} 解码出来是非 nil 的空集合,通过 required,想要“非空”要用 min=1;只含空格的字符串不是空串,也通过,需要 TrimSpace 或自定义规则;time.Time 的零值失败;嵌套结构体字段自身不被 required 检查,检查的是它里面的字段;指针结构体没传时报它自己。

表单与查询串

实测 · 本机执行输出
    /x?age=5&page=2&gt=1             → 通过 q.Age=5 q.Page=2
    /x?age=0&page=0&gt=0             → 不通过 F.Age(required) F.Gt(gt) q.Age=0 q.Page=0
    /x?age=&page=&gt=                → 不通过 F.Age(required) F.Gt(gt) q.Age=0 q.Page=0
    /x                               → 不通过 F.Age(required) F.Page(required) F.Gt(gt) q.Age=0 q.Page=nil
    /x?age=5&page=2&gt=1&opt=0       → 通过 q.Age=5 q.Page=2
    /x?age=5&page=2&gt=1&opt=-3      → 不通过 F.Opt(gt) q.Age=5 q.Page=2

表单绑定里缺失的键根本不会调用 setter,字段保持零值,所以 ?age=0、?age=、不传 age 三种情况对值类型的 required 没有区别,gt=0 同理。要让“不传”合法而“传了就必须合法”,用 omitempty,gt=0。

分步演示

required 与零值
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。校验器是 Gin 内置的 validator v10.30.1;JSON 请求用 httptest 直接调用 ServeHTTP,查询串用 ShouldBindQuery;hasValue 的源码来自模块缓存里 validator@v10.30.1 的 baked_in.go;每种情形各一次
    • 没有验证:validator 的 WithRequiredStructEnabled 选项(Gin 没有开启,没有跑);required_if / required_with 等条件规则对零值的处理;自定义类型实现 Zero 判断的情形;XML、YAML 绑定下的零值行为
    京ICP备2026067502号-1京公网安备11010502063487号