本节介绍如何将HTTP请求携带的数据绑定到go的自定义类型上。例如路径参数,GET请求参数,请求体,表单数据等等。

绑定表单数据到自定义struct

在struct的字段的标签中添加form标签,用来描述对应表单数据中的键。

type StructA struct {
    FieldA string `form:"field_a"`
}
 
type StructB struct {
    NestedStruct StructA
    FieldB string `form:"field_b"`
}
 
type StructC struct {
    NestedStructPointer *StructA
    FieldC string `form:"field_c"`
}
 
type StructD struct {
    NestedAnonyStruct struct {
        FieldX string `form:"field_x"`
    }
    FieldD string `form:"field_d"`
}
 
func GetDataB(c *gin.Context) {
    var b StructB
    c.Bind(&b)
    c.JSON(200, gin.H{
        "a": b.NestedStruct,
        "b": b.FieldB,
    })
}
 
func GetDataC(c *gin.Context) {
    var b StructC
    c.Bind(&b)
    c.JSON(200, gin.H{
        "a": b.NestedStructPointer,
        "c": b.FieldC,
    })
}
 
func GetDataD(c *gin.Context) {
    var b StructD
    c.Bind(&b)
    c.JSON(200, gin.H{
        "x": b.NestedAnonyStruct,
        "d": b.FieldD,
    })
}
 
func main() {
    r := gin.Default()
    r.GET("/getb", GetDataB)
    r.GET("/getc", GetDataC)
    r.GET("/getd", GetDataD)
 
    r.Run()
}

绑定请求体的数据到不同的struct

在实际使用中可能会遇到,如果请求体的数据符合一个struct,那么就绑定到这个struct上,如果不符合,则绑定到另一个struct上。 已知可以使用ShouldBind来绑定数据,但是下面这样的使用方式是错误的

type formA struct {
  Foo string `json:"foo" xml:"foo" binding:"required"`
}
 
type formB struct {
  Bar string `json:"bar" xml:"bar" binding:"required"`
}
 
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
  if errA := c.ShouldBind(&objA); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Always an error is occurred by this because c.Request.Body is EOF now.
  } else if errB := c.ShouldBind(&objB); errB == nil {
    c.String(http.StatusOK, `the body should be formB`)
  } else {
    ...
  }
}

这是因为调用ShouldBind绑定数据的时候,会读取出请求体中的数据,就算绑定失败,那么在第二次调用ShouldBind绑定到另一个struct的时候,无法读取到数据,那么第二次绑定永远都是事变。

要解决这个问题,使用ShouldBindBodyWith

func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // This reads c.Request.Body and stores the result into the context.
  if errA := c.ShouldBindBodyWith(&objA, binding.Form); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // At this time, it reuses body stored in the context.
  } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // And it can accepts other formats
  } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  } else {
    ...
  }
}

绑定表单数据到自定义的标签

前面我们用于数据绑定的struct,其对应的标签都是form, json, xml,但是如果希望能够使用自定义的标签来绑定对应的数据也是可以实现的。

需要定义用来自定义绑定的struct,来将自定义的标签与原来的form等做一个映射

const (
	customerTag = "url"
	defaultMemory = 32 << 20
)
 
type customerBinding struct {}
 
func (customerBinding) Name() string {
	return "form"
}
 
func (customerBinding) Bind(req *http.Request, obj interface{}) error {
	if err := req.ParseForm(); err != nil {
		return err
	}
	if err := req.ParseMultipartForm(defaultMemory); err != nil {
		if err != http.ErrNotMultipart {
			return err
		}
	}
	if err := binding.MapFormWithTag(obj, req.Form, customerTag); err != nil {
		return err
	}
	return validate(obj)
}
 
func validate(obj interface{}) error {
	if binding.Validator == nil {
		return nil
	}
	return binding.Validator.ValidateStruct(obj)
}

主要就是定义了一个类型,然后实现了Bind方法,在其中,将表单与自定义的标签进行了映射。例如上面的代码就会让表单数据会识别url标签的字段。

type FormA struct {
	FieldA string `url:"field_a"`
}
 
func ListHandler(s *Service) func(ctx *gin.Context) {
	return func(ctx *gin.Context) {
		var urlBinding = customerBinding{}
		var opt FormA
		err := ctx.MustBindWith(&opt, urlBinding)
		if err != nil {
			...
		}
		...
	}
}

tags: Web框架 gin