前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >echo 源码分析(数据绑定过程)

echo 源码分析(数据绑定过程)

作者头像
golangLeetcode
发布2022-08-02 19:10:21
1.1K0
发布2022-08-02 19:10:21
举报

我们知道http的参数传递的形式有很多,header、path、query、body,body( json,form)等等,针对各种形式的参数,通过bind方式来解析是比较清晰的方式,但是echo的bind 方式也是在从不完完善到逐渐完善的过程中,实践中你会发现,不同版本的echo,会出现诡异的结果,我将以下面的例子,针对v3.3.10、v4.1.17、v4.2.1三个版本的echo进行分析。

代码语言:javascript
复制
package main

import (
  "fmt"
  "net/http"

  "github.com/labstack/echo/v4"
)

type User struct {
  Name  string `json:"name" xml:"name` //param:"name" query:"name" form:"name"   //curl -XGET http://localhost:1323/users/Joe\?email\=joe_email
  Email string `json:"email" form:"email" query:"email"`
}

func main() {
  e := echo.New()
  e.GET("/users/:name", func(c echo.Context) error {
    u := new(User)
    u.Name = c.Param("name")
    if err := c.Bind(u); err != nil {
      return c.JSON(http.StatusBadRequest, nil)
    }
    return c.JSON(http.StatusOK, u)
  })
  fmt.Println("--------------------")
  e.GET("/users/:name/share/:id", func(c echo.Context) error {
    u := new(User)
    //u.Name = c.Param("name")
    if err := c.Bind(u); err != nil {
      return c.JSON(http.StatusBadRequest, nil)
    }
    return c.JSON(http.StatusOK, u)
  })
  fmt.Println("--------------------")
  e.GET("/users/names", func(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
      return c.JSON(http.StatusBadRequest, nil)
    }
    return c.JSON(http.StatusOK, u)
  })
  fmt.Println("--------------------")
  e.GET("/users/names/*", func(c echo.Context) error {
    u := new(User)
    if err := c.Bind(u); err != nil {
      return c.JSON(http.StatusBadRequest, nil)
    }
    return c.JSON(http.StatusOK, u)
  })
  fmt.Println(e.Start(":1336"))
}

如果我们引用

代码语言:javascript
复制
 "github.com/labstack/echo"

默认版本是v3.3.10

如果引用

代码语言:javascript
复制
 "github.com/labstack/echo/v4"

默认是最新版v4.2.1,但是v4.2.1和v4.1.17版本差异比较大,所以分析上述三个版本。

首先看下路由注册的过程

代码语言:javascript
复制
e.GET("/users/:name", func(c echo.Context) error {
    u := new(User)
    u.Name = c.Param("name")
    if err := c.Bind(u); err != nil {
      return c.JSON(http.StatusBadRequest, nil)
    }
    return c.JSON(http.StatusOK, u)
  })
代码语言:javascript
复制
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
  name := handlerName(handler)
  e.router.Add(method, path, func(c Context) error {
    h := handler
    // Chain middleware
    for i := len(middleware) - 1; i >= 0; i-- {
      h = middleware[i](h)
    }
    return h(c)
  })
  r := &Route{
    Method: method,
    Path:   path,
    Name:   name,
  }
  e.router.routes[method+path] = r
  return r
}
代码语言:javascript
复制
func (r *Router) Add(method, path string, h HandlerFunc) {
  // Validate path
  if path == "" {
    panic("echo: path cannot be empty")
  }
  if path[0] != '/' {
    path = "/" + path
  }
  pnames := []string{} // Param names
  ppath := path        // Pristine path

  for i, l := 0, len(path); i < l; i++ {
    if path[i] == ':' {
      j := i + 1

      r.insert(method, path[:i], nil, skind, "", nil)
      for ; i < l && path[i] != '/'; i++ {
      }

      pnames = append(pnames, path[j:i])
      path = path[:j] + path[i:]
      i, l = j, len(path)

      if i == l {
        r.insert(method, path[:i], h, pkind, ppath, pnames)
        return
      }
      r.insert(method, path[:i], nil, pkind, "", nil)
    } else if path[i] == '*' {
      r.insert(method, path[:i], nil, skind, "", nil)
      pnames = append(pnames, "*")
      r.insert(method, path[:i+1], h, akind, ppath, pnames)
      return
    }
  }

  r.insert(method, path, h, skind, ppath, pnames)
}

这里可以看到,在路由注册构建前缀树的过程中会把路由解析规整为三个类型,路径参数类型(:),精确匹配路由(/),正则匹配路由(*)

同时针对路径参数类型(:),会将路径中的参数名字保存在变量pnames里面,最终存在router的tree上

代码语言:javascript
复制
func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) {
  // Adjust max param
  l := len(pnames)
  if *r.echo.maxParam < l {
    *r.echo.maxParam = l
  }

  cn := r.tree // Current node as root
  if cn == nil {
    panic("echo: invalid method")
  }
  search := path

  for {
    sl := len(search)
    pl := len(cn.prefix)
    l := 0

    // LCP
    max := pl
    if sl < max {
      max = sl
    }
    for ; l < max && search[l] == cn.prefix[l]; l++ {
    }

    if l == 0 {
      // At root node
      cn.label = search[0]
      cn.prefix = search
      if h != nil {
        cn.kind = t
        cn.addHandler(method, h)
        cn.ppath = ppath
        cn.pnames = pnames
      }
    } else if l < pl {
      // Split node
      n := newNode(cn.kind, cn.prefix[l:], cn, cn.children, cn.methodHandler, cn.ppath, cn.pnames)

      // Reset parent node
      cn.kind = skind
      cn.label = cn.prefix[0]
      cn.prefix = cn.prefix[:l]
      cn.children = nil
      cn.methodHandler = new(methodHandler)
      cn.ppath = ""
      cn.pnames = nil

      cn.addChild(n)

      if l == sl {
        // At parent node
        cn.kind = t
        cn.addHandler(method, h)
        cn.ppath = ppath
        cn.pnames = pnames
      } else {
        // Create child node
        n = newNode(t, search[l:], cn, nil, new(methodHandler), ppath, pnames)
        n.addHandler(method, h)
        cn.addChild(n)
      }
    } else if l < sl {
      search = search[l:]
      c := cn.findChildWithLabel(search[0])
      if c != nil {
        // Go deeper
        cn = c
        continue
      }
      // Create child node
      n := newNode(t, search, cn, nil, new(methodHandler), ppath, pnames)
      n.addHandler(method, h)
      cn.addChild(n)
    } else {
      // Node already exists
      if h != nil {
        cn.addHandler(method, h)
        cn.ppath = ppath
        if len(cn.pnames) == 0 { // Issue #729
          cn.pnames = pnames
        }
      }
    }
    return
  }
}

接着我们看下,请求到来的时候,参数匹配的过程

代码语言:javascript
复制
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // Acquire context
  c := e.pool.Get().(*context)
  c.Reset(r, w)

  h := NotFoundHandler

  if e.premiddleware == nil {
    e.router.Find(r.Method, getPath(r), c)
    h = c.Handler()
    for i := len(e.middleware) - 1; i >= 0; i-- {
      h = e.middleware[i](h)
    }
  } else {
    h = func(c Context) error {
      e.router.Find(r.Method, getPath(r), c)
      h := c.Handler()
      for i := len(e.middleware) - 1; i >= 0; i-- {
        h = e.middleware[i](h)
      }
      return h(c)
    }
    for i := len(e.premiddleware) - 1; i >= 0; i-- {
      h = e.premiddleware[i](h)
    }
  }

  // Execute chain
  if err := h(c); err != nil {
    e.HTTPErrorHandler(err, c)
  }

  // Release context
  e.pool.Put(c)
}

其实就是到router中通过最长前缀匹配算法进行匹配

代码语言:javascript
复制
e.router.Find(r.Method, getPath(r), c)

其中getPath函数定义如下

代码语言:javascript
复制
func getPath(r *http.Request) string {
  path := r.URL.RawPath
  if path == "" {
    path = r.URL.Path
  }
  return path
}

find是路径匹配的过程

代码语言:javascript
复制

func (r *Router) Find(method, path string, c Context) {
  ctx := c.(*context)
  ctx.path = path
  cn := r.tree // Current node as root

  var (
    search  = path
    child   *node         // Child node
    n       int           // Param counter
    nk      kind          // Next kind
    nn      *node         // Next node
    ns      string        // Next search
    pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
  )

  // Search order static > param > any
  for {
    if search == "" {
      break
    }

    pl := 0 // Prefix length
    l := 0  // LCP length

    if cn.label != ':' {
      sl := len(search)
      pl = len(cn.prefix)

      // LCP
      max := pl
      if sl < max {
        max = sl
      }
      for ; l < max && search[l] == cn.prefix[l]; l++ {
      }
    }

    if l == pl {
      // Continue search
      search = search[l:]
    } else {
      cn = nn
      search = ns
      if nk == pkind {
        goto Param
      } else if nk == akind {
        goto Any
      }
      // Not found
      return
    }

    if search == "" {
      break
    }

    // Static node
    if child = cn.findChild(search[0], skind); child != nil {
      // Save next
      if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
        nk = pkind
        nn = cn
        ns = search
      }
      cn = child
      continue
    }

    // Param node
  Param:
    if child = cn.findChildByKind(pkind); child != nil {
      // Issue #378
      if len(pvalues) == n {
        continue
      }

      // Save next
      if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
        nk = akind
        nn = cn
        ns = search
      }

      cn = child
      i, l := 0, len(search)
      for ; i < l && search[i] != '/'; i++ {
      }
      pvalues[n] = search[:i]
      n++
      search = search[i:]
      continue
    }

    // Any node
  Any:
    if cn = cn.findChildByKind(akind); cn == nil {
      if nn != nil {
        cn = nn
        nn = cn.parent // Next (Issue #954)
        search = ns
        if nk == pkind {
          goto Param
        } else if nk == akind {
          goto Any
        }
      }
      // Not found
      return
    }
    pvalues[len(cn.pnames)-1] = search
    break
  }

  ctx.handler = cn.findHandler(method)
  ctx.path = cn.ppath
  ctx.pnames = cn.pnames

  // NOTE: Slow zone...
  if ctx.handler == nil {
    ctx.handler = cn.checkMethodNotAllowed()

    // Dig further for any, might have an empty value for *, e.g.
    // serving a directory. Issue #207.
    if cn = cn.findChildByKind(akind); cn == nil {
      return
    }
    if h := cn.findHandler(method); h != nil {
      ctx.handler = h
    } else {
      ctx.handler = cn.checkMethodNotAllowed()
    }
    ctx.path = cn.ppath
    ctx.pnames = cn.pnames
    pvalues[len(cn.pnames)-1] = ""
  }

  return
}

可以看到,将匹配到的值和路径参数名一一对应保存到了pvalues里面,最终是保存再来ctx里面

可以看到匹配过程中,会根据路径参数类型来进行处理

代码语言:javascript
复制
func (n *node) findChild(l byte, t kind) *node {
  for _, c := range n.children {
    if c.label == l && c.kind == t {
      return c
    }
  }
  return nil
}

有没有简单直接的方法来查看我们最终路由注册后pnames的存储结果和请求路径匹配过程中pvalues的参数匹配结果呢?可以在echo中,加下面几行代码,进行打印

代码语言:javascript
复制
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
  e.router.routes[method+path] = r
  printTree(e.router.tree)
  return r
}

其中printTree的定义如下

代码语言:javascript
复制
func printTree(tree *node) {
  v1, err1 := json.Marshal(struct {
    Kind          kind
    Label         byte
    Prefix        string
    Parent        *node
    Children      children
    ChildrenNum   int
    Ppath         string
    Pnames        []string
    MethodHandler *methodHandler
  }{
    Kind:          tree.kind,
    Label:         tree.label,
    Prefix:        tree.prefix,
    Parent:        tree.parent,
    Children:      tree.children,
    ChildrenNum:   len(tree.children),
    Ppath:         tree.ppath,
    Pnames:        tree.pnames,
    MethodHandler: tree.methodHandler,
  })
  fmt.Println(string(v1), err1)
  for i, v := range tree.children {
    fmt.Println(i)
    printTree(v)
  }
}

可以看到我们的路由注册结果

代码语言:javascript
复制
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}} <nil>
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}} <nil>
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}} <nil>
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{},{}],"ChildrenNum":2,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}} <nil>
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}} <nil>
1
{"Kind":0,"Label":110,"Prefix":"names","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/names","Pnames":[],"MethodHandler":{}} <nil>
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{},{}],"ChildrenNum":2,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}} <nil>
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}} <nil>
1
{"Kind":0,"Label":110,"Prefix":"names","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/names","Pnames":[],"MethodHandler":{}} <nil>
0
{"Kind":0,"Label":47,"Prefix":"/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}} <nil>
0
{"Kind":2,"Label":42,"Prefix":"*","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/names/*","Pnames":["*"],"MethodHandler":{}} <nil>

如何看参数匹配结果呢?同样处理

代码语言:javascript
复制

func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  e.router.Find(r.Method, getPath(r), c)
}

在find函数里加下面代码

代码语言:javascript
复制

func (r *Router) Find(method, path string, c Context) {
  v, err := json.Marshal(struct {
    //Request *http.Request
    //Response *Response
    Path    string
    Pnames  []string
    Pvalues []string
    Query   url.Values
    //Handler HandlerFunc
    Store Map
  }{
    //Request: ctx.request,
    //Response: ctx.response,
    Path:    ctx.path,
    Pnames:  ctx.pnames,
    Pvalues: ctx.pvalues,
    Query:   ctx.query,
    //Handler: ctx.handler,
    Store: ctx.store,
  })
  fmt.Println(string(v), err)
  return
}
代码语言:javascript
复制

//{"Path":"/users/:name","Pnames":["name"],"Pvalues":["Joe",""],"Query":null,"Store":null} <nil>
//{"Path":"/users/:name/share/:id","Pnames":["name","id"],"Pvalues":["Joe","1"],"Query":null,"Store":null} <nil>

这时候我们切换不同版本的echo,可以看到不同的结果

代码语言:javascript
复制
% curl -XGET http://localhost:1336/users/Joe/share\?email\=joe_email
{"message":"Not Found"}
% curl -XGET http://localhost:1336/users/Joe/share/1\?email\=joe_email
{"name":"Joe","email":"joe_email"}

首先看v3.3.10的实现

代码语言:javascript
复制
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
  if req.ContentLength == 0 {
        if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
        }
    }

    ctype := req.Header.Get(HeaderContentType)
  switch {
  case strings.HasPrefix(ctype, MIMEApplicationJSON):
    if err = json.NewDecoder(req.Body).Decode(i); err != nil {

        }
    }
}

这个实现是有问题的,因为即使是get请求,ContentLength也不会是0

代码语言:javascript
复制
% curl -i -XGET http://localhost:1336/users/Joe/share/1\?email\=joe_email
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Tue, 30 Mar 2021 03:40:22 GMT
Content-Length: 35
{"name":"","email":"joe_email"}

针对contentlength=0的情况,调用了bindData方法

代码语言:javascript
复制

func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
  typ := reflect.TypeOf(ptr).Elem()
  val := reflect.ValueOf(ptr).Elem()
    for i := 0; i < typ.NumField(); i++ {
        inputFieldName := typeField.Tag.Get(tag)

              // If tag is nil, we inspect if the field is a struct.
      if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct {
        if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
                }
            }

            inputValue, exists := data[inputFieldName]

    }
}

里面其实是应用了反射,对结构体的值进行了修改,可以看到,如果header里contentlength不为零,路径里的参数根本匹配不上

其中QueryParams()返回的是url里面的值

代码语言:javascript
复制
QueryParams() url.Values

我们升级到v4.1.17看看

代码语言:javascript
复制
% go get -u github.com/labstack/echo/v4@v4.1.17
go: finding module for package github.com/labstack/echo
代码里引用的地方也要由
"github.com/labstack/echo"
改成
"github.com/labstack/echo/v4"
否则会
go: found github.com/labstack/echo in github.com/labstack/echo v3.3.10+incompatible

路径参数绑定成功了

代码语言:javascript
复制
 % curl -i -XGET http://localhost:1336/users/Joe/share/1\?email\=joe_email
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Tue, 30 Mar 2021 05:21:10 GMT
Content-Length: 35

{"name":"Joe","email":"joe_email"}

我们发现参数绑定方法已经重写了

代码语言:javascript
复制
// Bind implements the `Binder#Bind` function.
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
  req := c.Request()

  names := c.ParamNames()
  values := c.ParamValues()
  params := map[string][]string{}
  for i, name := range names {
    params[name] = []string{values[i]}
  }
  if err := b.bindData(i, params, "param"); err != nil {
    return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
  }
  if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
    return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
  }
  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)
    }
  }
  return
}

依次会对路径参数param,query参数query,以及body进行绑定,body绑定依赖http的header

下面是bindData函数

代码语言:javascript
复制
func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
  if ptr == nil || len(data) == 0 {
    return nil
  }
  typ := reflect.TypeOf(ptr).Elem()
  val := reflect.ValueOf(ptr).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 {
    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 == "" {
      inputFieldName = typeField.Name //在4.2.1中删除了这个字段
      // If tag is nil, we inspect if the field is a struct.
      if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
        if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
          return err
        }
        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
}

可以看到,在匹配路径参数的过程中,如果结构体的tag里没有param,会选取结构体的参数名,到路径参数里去取对应的value

代码语言:javascript
复制
    inputFieldName := typeField.Tag.Get(tag)

    if inputFieldName == "" {
      inputFieldName = typeField.Name //在4.2.1中删除了这个字段
      // If tag is nil, we inspect if the field is a struct.
      if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
        if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
          return err
        }
        continue
      }
    }

也就是说,struct的tag即使不正确,也是可能匹配到正确参数的

最后看看v4.2.1的实现

代码语言:javascript
复制
go get -u github.com/labstack/echo/v4
代码语言:javascript
复制
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
  if err := b.BindPathParams(c, i); err != nil {
    return err
  }
      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)
}
代码语言:javascript
复制

func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
  names := c.ParamNames()
  values := c.ParamValues()
  params := map[string][]string{}
  for i, name := range names {
    params[name] = []string{values[i]}
  }
  if err := b.bindData(i, params, "param"); err != nil {
    return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
  }
  return nil
}


func (b *DefaultBinder) BindQueryParams(c Context, i interface{}) error {
  if err := b.bindData(i, c.QueryParams(), "query"); err != nil {
    return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
  }
  return nil
}


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 {
        }
    }
}
代码语言:javascript
复制

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 {
    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  //注意从哪部移动出来了,所以,没有tag就不继续了
    }

    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
}

可以看到,匹配过程更严格了,严格要求按照结构体tag定义来匹配

代码语言:javascript
复制
 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  //注意从哪部移动出来了,所以,没有tag就不继续了
    }

好处是什么呢?可以处理同名参数

这两个版本可以具体diff一下看看改动

代码语言:javascript
复制
 vimdiff ~/go/pkg/mod/github.com/labstack/echo/v4@v4.1.17/bind.go ~/go/pkg/mod/github.com/labstack/echo/v4@v4.2.1/bind.go 
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-03-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档