在web开发中一个不可避免的环节就是对请求参数进行校验,通常我们会在代码中定义与请求参数相对应的模型(结构体),借助模型绑定快捷地解析请求中的参数,例如 gin 框架中经常使用validator库对参数进行校验。validator库的具体使用可以参考官网:validator

基本示例

  • main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    type Login struct {
    	RequestData string `form:"requestData" json:"requestData" binding:"-"`
    }
    
    func main() {
    	r := gin.Default()
    	r.GET("/hello", func(c *gin.Context) {
    		var login Login
    		// ShouldBind()会根据请求的Content-Type自行选择绑定器
    		if err := c.ShouldBind(&login); err == nil {
    			c.JSON(http.StatusOK, gin.H{
    				"RequestData":     login.RequestData,
    			})
    		} else {
    			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    		}
    	})
    	err := r.Run(":8081")
    	if err != nil {
    		return
    	}
    }
    
    // http://127.0.0.1:8081/hello
    {
      "requestData": ""
    }
    
    // http://127.0.0.1:8081/hello?requestData=wanna
    {
      "requestData": "wanna"
    }
    

    请求参数默认值是开发中的常见需求,如果前端没有传相关参数,我们可以自己设置一个默认值。Gin 框架提供了方法,例如:DefaultQuery,DefaultPostForm等,还可以在逻辑中自己判断补充,但是这未免也太麻烦了,请求参数的类型是各种各样的,场景太多了,我们可不可以在校验参数的时候设置默认值呢?

    validator库没有直接提供设置默认值的方式,如果没有传,那么参数默认为定义类型的零值,例如:

    数据类型对应零值
    string空字符串
    int0
    float0.0
    booleanfalse

    上述示例中,我没有传requestData的值,所以返回的是空字符串。

使用反射设置默认值

我们可以使用反射获取struct字段中的 tag,然后设置默认值

  • util.go
// 设置默认值的通用函数
func setDefaults(obj interface{}) {
	v := reflect.ValueOf(obj).Elem()
	setDefaultsRecursive(v)
}

func setDefaultsRecursive(v reflect.Value) {
	// 处理指针类型
	if v.Kind() == reflect.Ptr {
		v = v.Elem()
	}

	// 处理结构体类型
	if v.Kind() == reflect.Struct {
		t := v.Type()

		for i := 0; i < t.NumField(); i++ {
			field := t.Field(i)
			value := v.Field(i)

			// 处理嵌套结构体
			if value.Kind() == reflect.Struct && field.Anonymous {
				setDefaultsRecursive(value)
				continue
			}

			// 处理字段默认值
			if tagValue, ok := field.Tag.Lookup("default"); ok && value.CanSet() {
				if value.IsZero() {
					switch value.Kind() {
					case reflect.String:
						value.SetString(tagValue)
					case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
						if intValue, err := strconv.ParseInt(tagValue, 10, 64); err == nil {
							value.SetInt(intValue)
						}
					case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
						if uintValue, err := strconv.ParseUint(tagValue, 10, 64); err == nil {
							value.SetUint(uintValue)
						}
					case reflect.Float32, reflect.Float64:
						if floatValue, err := strconv.ParseFloat(tagValue, 64); err == nil {
							value.SetFloat(floatValue)
						}
					case reflect.Bool:
						if boolValue, err := strconv.ParseBool(tagValue); err == nil {
							value.SetBool(boolValue)
						}
						// 这里可以添加更多类型处理
					}
				}
			}
		}
	}
}

// BindWithDefaults 自定义绑定器以处理默认值
func BindWithDefaults(c *gin.Context, obj interface{}) error {
	if err := c.ShouldBind(obj); err != nil {
		return err
	}
	setDefaults(obj)
	return nil
}
  • 修改示例代码
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

type Login struct {
    // 添加 default tag 设置默认值
    RequestData string `form:"requestData" json:"requestData" binding:"-" default:"default"`
}

func main() {
	r := gin.Default()
	r.GET("/hello", func(c *gin.Context) {
		var login Login
		// 修改为自定义的绑定器
		if err := BindWithDefaults(c, &login); err == nil {
			c.JSON(http.StatusOK, gin.H{
				"RequestData":     login.RequestData,
			})
		} else {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		}
	})
	err := r.Run(":8081")
	if err != nil {
		return
	}
}
// http://127.0.0.1:8081/hello
{
  "requestData": "default"
}

// http://127.0.0.1:8081/hello?requestData=wanna
{
  "requestData": "wanna"
}