Golang中c.Bind()方法的意外行为解析

Golang中c.Bind()方法的意外行为解析 我想将数据绑定到的结构体是:

type Form_Eingabe struct {
	Eingabe1 float64 `json:"eingabe1" form:"eingabe1" binding:"required,numeric,gt=0,max=1000000000"`
	Eingabe2 float64 `json:"eingabe2" form:"eingabe2" binding:"required,numeric,gt=0,max=99"`
}

基本上一切正常,但我想检查我的应用程序,以防某些人可能发送错误信息。具体来说,就是发送一个**string** 而不是 float

我的处理流程如下:

  1. c.Bind()
  2. 使用验证器进行验证

正如我所说,如果你发送浮点数值,一切正常,但如果你发送一个字符串,一切都会出错。

以下是一些示例:

示例 1 (正常工作)

输入:

  • eingabe1 = 1000
  • eingabe2 = 12.2

输出:

  • eingabe1 = 1000
  • eingabe2 = 12.2

示例 2 (完全中断)

输入:

  • eingabe1 = “some string”
  • eingabe2 = 12.2

输出:

  • eingabe1 = 0
  • eingabe2 = 0

示例 3 (部分中断)

输入:

  • eingabe1 = 1000
  • eingabe2 = “some string”

输出:

  • eingabe1 = 1000
  • eingabe2 = 0

根据这些数据(我经过调试和分析),c.Bind() 会在第一个错误发生后停止处理任何新数据。所以,如果我有 5 个值,错误发生在第一个,那么所有数据都是 0;如果错误发生在第三个,那么从第三个开始的所有数据都是 0,但之前的所有数据都会被设置。即使第 4 个和第 5 个值再次有效,它们也不会被设置!

错误值被设置为 0 我没有问题,但我有问题的是:

  1. 从错误发生点开始的所有值(无论是否有效)也都会被设置为 0。
  2. 没有为此输入设置任何错误,而只是触发了一个单一的一般性错误。

我的绑定和验证部分如下所示:

[...]
if err := c.Bind(&form); err != nil {
	var ve validator.ValidationErrors

	if errors.As(err, &ve) {
		out = make([]Fehler_entry, len(ve))
		for i, fe := range ve {
			out[i] = Fehler_entry{fe.Field(), getErrorMsg(fe)}
			F = append(F, out[i])
		}

		fmt.Println(F)

		p = Messages{Debug: D, Fehler: F}
		c.HTML(200, "main.html", p)
		return
	}
	[...]
}
[...]

所以,如果有人填写了一个字符串,我只会得到这条消息: strconv.ParseFloat: parsing "some string": invalid syntax,但没有任何信息表明是哪个字段触发了这个错误。

如果所有绑定都正常,只是验证失败,我会得到像这样的良好错误信息: Key: 'Form_Eingabe.Eingabe1' Error:Field validation for 'Eingabe1' failed on the 'max' tag

这清楚地告诉我哪个字段以及为什么出错。

我添加了一个验证器 “numeric”,所以 int 和 float 都被接受,但由于验证发生在绑定之后,所以这个验证器永远不会运行。 有谁知道我如何修复这个问题,以便当一个字段绑定失败时,我能确切知道是哪一个,并且如何让它不中断,而是跳过这个字段并设置一个错误,最好与我从验证器本身得到的错误非常相似。

我知道这要求很多,但我已经花了整整两天时间尝试修复,却不知道该如何做。


更多关于Golang中c.Bind()方法的意外行为解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中c.Bind()方法的意外行为解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


c.Bind() 在遇到类型转换错误时会停止处理后续字段,这是设计行为。要解决这个问题,需要使用 c.ShouldBind() 配合自定义绑定器。以下是具体实现:

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
    "strconv"
)

// 自定义绑定器
type CustomBinder struct {
    binding.Binding
}

func (cb CustomBinder) Bind(c *gin.Context, obj interface{}) error {
    // 先尝试标准绑定
    if err := c.ShouldBind(obj); err != nil {
        // 如果是类型转换错误,继续处理其他字段
        if _, ok := err.(*strconv.NumError); ok {
            // 手动处理表单数据
            return cb.bindFormData(c, obj)
        }
        return err
    }
    return nil
}

func (cb CustomBinder) bindFormData(c *gin.Context, obj interface{}) error {
    // 使用反射手动处理每个字段
    v := reflect.ValueOf(obj).Elem()
    t := v.Type()
    
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        fieldType := t.Field(i)
        
        // 获取表单标签
        formTag := fieldType.Tag.Get("form")
        if formTag == "" {
            continue
        }
        
        // 获取表单值
        formValue := c.PostForm(formTag)
        if formValue == "" {
            continue
        }
        
        // 根据字段类型转换
        switch field.Kind() {
        case reflect.Float64:
            if val, err := strconv.ParseFloat(formValue, 64); err == nil {
                field.SetFloat(val)
            } else {
                // 记录错误但不中断
                field.SetFloat(0)
            }
        // 添加其他类型处理...
        }
    }
    
    return nil
}

// 使用自定义绑定器
func main() {
    r := gin.Default()
    
    // 注册自定义绑定器
    binding.Validator = &CustomBinder{}
    
    r.POST("/test", func(c *gin.Context) {
        var form Form_Eingabe
        
        // 使用自定义绑定
        if err := c.ShouldBindWith(&form, &CustomBinder{}); err != nil {
            // 处理错误
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        // 手动验证
        if err := validateForm(form); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        c.JSON(200, form)
    })
    
    r.Run()
}

// 手动验证函数
func validateForm(form Form_Eingabe) error {
    validate := validator.New()
    
    // 注册自定义验证器
    validate.RegisterValidation("numeric", func(fl validator.FieldLevel) bool {
        // 检查是否为数字
        _, err := strconv.ParseFloat(fmt.Sprintf("%v", fl.Field()), 64)
        return err == nil
    })
    
    return validate.Struct(form)
}

另一种更简洁的方法是使用中间件预处理数据:

func PreprocessFormData() gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.Method == "POST" {
            // 复制原始表单数据
            c.Request.ParseForm()
            formData := make(map[string]interface{})
            
            for key, values := range c.Request.PostForm {
                if len(values) > 0 {
                    // 尝试转换为float64
                    if val, err := strconv.ParseFloat(values[0], 64); err == nil {
                        formData[key] = val
                    } else {
                        // 记录转换错误
                        formData[key] = 0
                        // 存储错误信息
                        c.Set(key+"_error", "invalid number format")
                    }
                }
            }
            
            // 将处理后的数据存储到上下文中
            c.Set("processed_form", formData)
        }
        
        c.Next()
    }
}

// 在路由中使用
r.Use(PreprocessFormData())

r.POST("/submit", func(c *gin.Context) {
    var form Form_Eingabe
    
    // 从上下文获取处理后的数据
    if processed, exists := c.Get("processed_form"); exists {
        formData := processed.(map[string]interface{})
        
        // 手动绑定到结构体
        if val, ok := formData["eingabe1"]; ok {
            form.Eingabe1 = val.(float64)
        }
        if val, ok := formData["eingabe2"]; ok {
            form.Eingabe2 = val.(float64)
        }
        
        // 检查错误
        if err1, exists := c.Get("eingabe1_error"); exists {
            // 处理字段特定错误
            fmt.Println("Eingabe1 error:", err1)
        }
    }
    
    // 继续验证
    validate := validator.New()
    if err := validate.Struct(form); err != nil {
        // 处理验证错误
    }
    
    c.JSON(200, form)
})

对于验证错误信息的收集,可以这样实现:

type FieldError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func CollectValidationErrors(err error) []FieldError {
    var errors []FieldError
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, fe := range validationErrors {
            errors = append(errors, FieldError{
                Field:   fe.Field(),
                Message: getErrorMessage(fe),
            })
        }
    } else if numErr, ok := err.(*strconv.NumError); ok {
        // 处理数字转换错误
        errors = append(errors, FieldError{
            Field:   "unknown", // 需要从上下文中获取字段名
            Message: fmt.Sprintf("Invalid number format: %s", numErr.Num),
        })
    }
    
    return errors
}

这些方法可以解决字段绑定中断问题和错误信息不明确的问题。

回到顶部