Go Echo 学习笔记(十三)进阶知识


进阶知识

Echo框架定制

自定义 Binder

Echo 中的 Binder 用于处理请求数据的绑定任务。

对于 Binder,Echo 默认提供了一个实现:echo.DefaultBinder,通常情况下,这个默认实现能够满足要求。

首先来看下 Binder 的实现。

首先,Echo 定义了一个接口:

type Binder interface{
  Bind(i interface{}, c Context) error
}

任何 Binder 必须实现该接口,也就是提供 Bind 方法。

一起看看 DefaultBinder 的 Bind 方法实现:

func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
	if err := b.BindPathParams(c, i); err != nil {
		return err
	}
	// Issue #1670 - Query params are binded only for GET/DELETE and NOT for usual request with body (POST/PUT/PATCH)
	// Reasoning here is that parameters in query and bind destination struct could have UNEXPECTED matches and results due that.
	// i.e. is `&id=1&lang=en` from URL same as `{"id":100,"lang":"de"}` request body and which one should have priority when binding.
	// This HTTP method check restores pre v4.1.11 behavior and avoids different problems when query is mixed with body
	if c.Request().Method == http.MethodGet || c.Request().Method == http.MethodDelete {
		if err = b.BindQueryParams(c, i); err != nil {
			return err
		}
	}
	return b.BindBody(c, i)
}

func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
	req := c.Request()
	if req.ContentLength == 0 {
		return
	}

	ctype := req.Header.Get(HeaderContentType)
	switch {
	case strings.HasPrefix(ctype, MIMEApplicationJSON):
		if err = json.NewDecoder(req.Body).Decode(i); err != nil {
			if ute, ok := err.(*json.UnmarshalTypeError); ok {
				return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err)
			} else if se, ok := err.(*json.SyntaxError); ok {
				return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err)
			}
			return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
		}
	case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
		if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
			if ute, ok := err.(*xml.UnsupportedTypeError); ok {
				return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err)
			} else if se, ok := err.(*xml.SyntaxError); ok {
				return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error())).SetInternal(err)
			}
			return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
		}
	case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
		params, err := c.FormParams()
		if err != nil {
			return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
		}
		if err = b.bindData(i, params, "form"); err != nil {
			return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
		}
	default:
		return ErrUnsupportedMediaType
	}
	return nil
}

func (b *DefaultBinder) bindData(destination interface{}, data map[string][]string, tag string) error {
	if destination == nil || len(data) == 0 {
		return nil
	}
	typ := reflect.TypeOf(destination).Elem()
	val := reflect.ValueOf(destination).Elem()

	// Map
	if typ.Kind() == reflect.Map {
		for k, v := range data {
			val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
		}
		return nil
	}

	// !struct
	if typ.Kind() != reflect.Struct {
		if tag == "param" || tag == "query" {
			// incompatible type, data is probably to be found in the body
			return nil
		}
		return errors.New("binding element must be a struct")
	}

	for i := 0; i < typ.NumField(); i++ {
		typeField := typ.Field(i)
		structField := val.Field(i)
		if !structField.CanSet() {
			continue
		}
		structFieldKind := structField.Kind()
		inputFieldName := typeField.Tag.Get(tag)

		if inputFieldName == "" {
			// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
			// structs that implement BindUnmarshaler are binded only when they have explicit tag
			if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
				if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
					return err
				}
			}
			// does not have explicit tag and is not an ordinary struct - so move to next field
			continue
		}

		inputValue, exists := data[inputFieldName]
		if !exists {
			// Go json.Unmarshal supports case insensitive binding.  However the
			// url params are bound case sensitive which is inconsistent.  To
			// fix this we must check all of the map values in a
			// case-insensitive search.
			for k, v := range data {
				if strings.EqualFold(k, inputFieldName) {
					inputValue = v
					exists = true
					break
				}
			}
		}

		if !exists {
			continue
		}

		// Call this first, in case we're dealing with an alias to an array type
		if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok {
			if err != nil {
				return err
			}
			continue
		}

		numElems := len(inputValue)
		if structFieldKind == reflect.Slice && numElems > 0 {
			sliceOf := structField.Type().Elem().Kind()
			slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
			for j := 0; j < numElems; j++ {
				if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
					return err
				}
			}
			val.Field(i).Set(slice)
		} else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
			return err

		}
	}
	return nil
}

其中:

  • DefaultBinder 使用 bindData 方法进行实际的数据绑定,主要通过反射进行处理,要求被绑定的类型是 map[string]interface{}struct(实际是实现它们的指针);
  • 通过给 Struct 的字段加上不同的 Tag 来接收不同类型的值:
    • param tag 对应路径参数;
    • query tag 对应 URL 参数;
    • json tag 对应 application/json 方式参数;
    • form tag 对应 POST 表单数据;
    • xml tag 对应 application/xml 或 text/xml;
  • 从代码的顺序可以看出,当同一个字段在多种方式存在值时,优先级顺序:param < query < 其他;

使用 Echo 的 DefaultBinder 进行请求数据绑定的时候只需要在声明一个请求对象 type 的时候在对应的参数后面添加相应的 tag:

type User struct {
    Name string `query:"name" form:"name" json:"name" xml:"name"`
    Sex  string `query:"sex" form:"sex" json:"sex" xml:"sex"`
}

这样只需在请求处理器当中对请求数据进行绑定就可以将请求数据绑定到一个新建的结构体对象上了:

func TestBinder(c echo.Context) error{
    user := &User{}
    if err := c.Bind(user);err != nil{
        return err
    }
    return c.Json(http.StatusOK,user)
}

DefaultBinder 已经可以满足针对 query、form、path、json、xml 几种数据类型的绑定工作,但是对于其他数据类型的绑定工作就不支持了。

比如前台使用 msgpack 格式来传递数据的话就得需要自定义 Binder。

自定义 MsgpackBinder 并实现 Bind 接口:

type MsgpackBinder struct {
}

// Bind 实现接口
func (binder *MsgpackBinder) Bind(i interface{}, c echo.Context) (err error) {

	// 支持默认的binder
	db := new(echo.DefaultBinder)
	if err1 := db.Bind(i, c); err1 != echo.ErrUnsupportedMediaType {
		// 默认解析成功返回
		return
	}
	// 获取请求数据
	request := c.Request()
	// 获取请求数据类型
	ctype := request.Header.Get(echo.HeaderContentType)
	// 针对msgpack类型的数据进行解析
	if strings.HasPrefix(ctype, echo.MIMEApplicationMsgpack) {
		if err := msgpack.NewDecoder(request.Body).Decode(i); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
		}
		// 解析成功返回
		return
	}
	// 不支持的类型
	return echo.ErrUnsupportedMediaType
}

设置 Echo 的 Binder:

func main() {
	e := echo.New()
	// 使用自定义Binder
	e.Binder = new(models.MsgpackBinder)
	e.POST("/", func(context echo.Context) error {
		user := new(models.User)
		if err := context.Bind(user); err != nil {
			return err
		}
		return context.JSON(http.StatusOK, user)
	})
	e.Logger.Fatal(e.Start(":8080"))
}

模拟用户发送 Msgpack 类型的数据:

func main() {
	// 发送数据
	type User struct {
		Name string
		Sex  string
	}

	data, err := msgpack.Marshal(&User{Name: "wangxin", Sex: "male"})
	if err != nil {
		panic(err)
	}

	response, err := http.DefaultClient.Post("http://localhost:8080/", "application/msgpack", bytes.NewReader(data))
	if err != nil {
		panic(err)
	}
	// 延迟关闭读写流
	defer response.Body.Close()

	// 读取响应数据
	result, err := ioutil.ReadAll(response.Body)
	if err != nil {
		panic(err)
	}
	// 输出响应数据
	fmt.Printf("%s\n", result)
}

运行成功获取到返回过来的 Json 数据。