关于 gin 的 Context.ShouldBind 对结构体 tag `binding:"required"` 校验失效问题(缺失字段未报错)

1. 运行环境

go1.19.13 windows/amd64
github.com/gin-gonic/gin v1.9.0

2. 问题描述?

Context.ShouldBind 对结构体 tag binding:"required" 校验失效:缺失字段未报错
示例代码:

// package form
type UpdateRequest2 struct {
    Sdagfadgasasdgsafdfsdafs string `form:"sdagfadgasasdgsafdfsdafs" binding:"required"`
}

// package api
func (Analysis) Update(ctx *gin.Context) {
    fmt.Println("================Update===========")
    params := &form.UpdateRequest2{}
    errA := ctx.ShouldBind(params)
    fmt.Println("bindA: ", errA)
    errB := ctx.ShouldBind(&params)
    fmt.Println("bindB: ", errB)
    errC := ctx.ShouldBind(*params)
    fmt.Println("bindC: ", errC)
    fmt.Println(ctx.Request.Form)
}

通过 postman 进行PUT测试 form-data 等数据均留空,结果如下:

================Update===========
bindA:  Key: 'UpdateRequest2.Sdagfadgasasdgsafdfsdafs' Error:Field validation for 'Sdagfadgasasdgsafdfsdafs' failed on the 'required' tag
bindB:  <nil>
bindC:  Key: 'UpdateRequest2.Sdagfadgasasdgsafdfsdafs' Error:Field validation for 'Sdagfadgasasdgsafdfsdafs' failed on the 'required' tag
map[]

如上所示当 ShouldBind 的参数是 结构体指针的指针就无法正确校验,传的是结构体和其指针就正常。好奇就看了下源码,但感觉应该不会如此(最终都会还原成结构体来校验),所以写了一个测试用例

func TestBind2(t *testing.T) {
    ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
    ctx.Request = httptest.NewRequest("PUT", "http://127.0.0.1:8003/test", nil)
    params := &form.UpdateRequest2{}
    errA := ctx.ShouldBind(params)
    fmt.Println("bindA: ", errA)
    errB := ctx.ShouldBind(&params)
    fmt.Println("bindB: ", errB)
    errC := ctx.ShouldBind(*params)
    fmt.Println("bindC: ", errC)
    fmt.Println(ctx.Request.Form)
}

结果如下

bindA:  Key: 'UpdateRequest2.Sdagfadgasasdgsafdfsdafs' Error:Field validation for 'Sdagfadgasasdgsafdfsdafs' failed on the 'required' tag
bindB:  Key: 'UpdateRequest2.Sdagfadgasasdgsafdfsdafs' Error:Field validation for 'Sdagfadgasasdgsafdfsdafs' failed on the 'required' tag
bindC:  Key: 'UpdateRequest2.Sdagfadgasasdgsafdfsdafs' Error:Field validation for 'Sdagfadgasasdgsafdfsdafs' failed on the 'required' tag
map[]

两次测试 ctx.Request.Form 都是空的
测试用例中三种情况都能够正常进行校验,不理解为什么会造成这种差异。
是否有其他地方影响了 ShouldBind 的校验?

3.目标

找到造成两种情况结果不同的原因。

最佳答案

问题找到了,项目里面的代码修改了验证器,而那个自定义的验证器没有递归的处理 obj 导致多层指针的情况下没有成功验证 :see_no_evil:

5个月前 评论
讨论数量: 3

首先你要理解golang中的*&含义:

  • *代表是对指针取值,取的是指针的值。
  • &代表的是获取变脸的地址。获取的是变量内存地址。

你的param定义的就是params := &form.UpdateRequest2{},已经是一个指针类型了, 你传入ShouldBind的时候,

  1. 第一个传入的是param,是一个指针,这是一个正确的操作,ginshouldBind会修改这个指针对应的结构体UpdateRequest2的值,也会拿到对应的tag
  2. 第二个传入的是&param,获取的是变量的内存地址,你传入一个内存地址,shouldBind拿到的是一个内存地址,所以并不会拿到UpdateRequest2这个结构体。
  3. 第三个传入的是*param,这是个取值操作,取的是param这个指针的值,还是UpdateRequest2这个结构体。但是

但是!第三个是有问题的!你传的是结构体,并不是指针,gin没法修改,如果你不传空,肯定会报错。 所以总的来说还是你对*&理解有问题。

5个月前 评论

@ezreal_rao

第三个是有问题的!你传的是结构体,并不是指针,gin 没法修改,如果你不传空,肯定会报错。

第三个确实是有问题的,写测试用例的时候没有注意。但这个问题的重点不是如何正确的绑定,而是为什么在项目中第二种写法( &params 指针的指针)没有正确的进行 tag 校验。但在测试用例中又能够正确的校验。

第二个传入的是 &param,获取的是变量的内存地址,你传入一个内存地址,shouldBind 拿到的是一个内存地址,所以并不会拿到 UpdateRequest2 这个结构体。

关于这点在 ginValidator.ValidateStruct 是不成立的,相关源码如下

// github.com\gin-gonic\gin@v1.9.1\binding\default_validator.go
func (v *defaultValidator) ValidateStruct(obj any) error {
    if obj == nil {
        return nil
    }

    value := reflect.ValueOf(obj)
    switch value.Kind() {
    case reflect.Ptr:
        return v.ValidateStruct(value.Elem().Interface())
    case reflect.Struct:
        return v.validateStruct(obj)
    case reflect.Slice, reflect.Array:
        count := value.Len()
        validateRet := make(SliceValidationError, 0)
        for i := 0; i < count; i++ {
            if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
                validateRet = append(validateRet, err)
            }
        }
        if len(validateRet) == 0 {
            return nil
        }
        return validateRet
    default:
        return nil
    }
}

ValidateStruct 中如果 obj 是指针会继续递归的进行校验,直到 obj 是有效类型才会进行真正的校验, 所以即使传递的是指针的指针也是可以获取结构体的。

我的疑问是 为什么同样的代码在测试用例中都能够正确的校验(该问题针对于:binding:”required”),而项目中使用指针可以,指针的指针不行,是否存在其他因素影响了校验?

5个月前 评论

问题找到了,项目里面的代码修改了验证器,而那个自定义的验证器没有递归的处理 obj 导致多层指针的情况下没有成功验证 :see_no_evil:

5个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!