前端传了 {"age":0},后端却说 age 是必填?binding:"required" 对零值的判定常常出乎意料。这篇从 validator 的 hasValue 源码出发,用 JSON 和查询串把值类型、指针、切片、map、字符串、时间、嵌套结构体的“零值”与“没传”对比一遍。
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>=1 → 通过 q.Age=5 q.Page=2
/x?age=0&page=0>=0 → 不通过 F.Age(required) F.Gt(gt) q.Age=0 q.Page=0
/x?age=&page=>= → 不通过 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>=1&opt=0 → 通过 q.Age=5 q.Page=2
/x?age=5&page=2>=1&opt=-3 → 不通过 F.Opt(gt) q.Age=5 q.Page=2表单绑定里缺失的键根本不会调用 setter,字段保持零值,所以 ?age=0、?age=、不传 age 三种情况对值类型的 required 没有区别,gt=0 同理。要让“不传”合法而“传了就必须合法”,用 omitempty,gt=0。