bind echo 数据绑定

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



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”))
}
如果我们引用



“github.com/labstack/echo”
默认版本是v3.3.10



如果引用



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



首先看下路由注册的过程



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)
})
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 = middlewarei
}
return h(c)
})
r := &Route{
Method: method,
Path: path,
Name: name,
}
e.router.routes[method+path] = r
return r
}
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上



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 } } 接着我们看下,请求到来的时候,参数匹配的过程


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.middlewarei
}
} 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.middlewarei
}
return h(c)
}
for i := len(e.premiddleware) - 1; i >= 0; i– {
h = e.premiddlewarei
}
}



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



// Release context
e.pool.Put(c)
}
其实就是到router中通过最长前缀匹配算法进行匹配



e.router.Find(r.Method, getPath(r), c)
其中getPath函数定义如下



func getPath(r *http.Request) string {
path := r.URL.RawPath
if path == “” {
path = r.URL.Path
}
return path
}
find是路径匹配的过程



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里面



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



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中,加下面几行代码,进行打印



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的定义如下



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)
}
}
可以看到我们的路由注册结果



{“Kind”:0,”Label”:47,”Prefix”:”/users/”,”Parent”:null,”Children”:[{}],”ChildrenNum”:1,”Ppath”:””,”Pnames”:null,”MethodHandler”:{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}}
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}}
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}}
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{},{}],"ChildrenNum":2,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}}
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}}
1
{"Kind":0,"Label":110,"Prefix":"names","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/names","Pnames":[],"MethodHandler":{}}
--------------------
{"Kind":0,"Label":47,"Prefix":"/users/","Parent":null,"Children":[{},{}],"ChildrenNum":2,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/:name","Pnames":["name"],"MethodHandler":{}}
0
{"Kind":0,"Label":47,"Prefix":"/share/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":1,"Label":58,"Prefix":":","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/:name/share/:id","Pnames":["name","id"],"MethodHandler":{}}
1
{"Kind":0,"Label":110,"Prefix":"names","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"/users/names","Pnames":[],"MethodHandler":{}}
0
{"Kind":0,"Label":47,"Prefix":"/","Parent":{},"Children":[{}],"ChildrenNum":1,"Ppath":"","Pnames":null,"MethodHandler":{}}
0
{"Kind":2,"Label":42,"Prefix":"*","Parent":{},"Children":null,"ChildrenNum":0,"Ppath":"/users/names/*","Pnames":["*"],"MethodHandler":{}}
如何看参数匹配结果呢?同样处理



func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e.router.Find(r.Method, getPath(r), c)
}
在find函数里加下面代码



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
}



//{“Path”:”/users/:name”,”Pnames”:[“name”],”Pvalues”:[“Joe”,””],”Query”:null,”Store”:null}
//{"Path":"/users/:name/share/:id","Pnames":["name","id"],"Pvalues":["Joe","1"],"Query":null,"Store":null}
这时候我们切换不同版本的echo,可以看到不同的结果



% 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的实现



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


% 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方法



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里面的值



QueryParams() url.Values
我们升级到v4.1.17看看



% 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
路径参数绑定成功了



% 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”}
我们发现参数绑定方法已经重写了



// 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函数



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

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的实现



go get -u github.com/labstack/echo/v4
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)
}



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 {
}
}
}



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定义来匹配


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一下看看改动



vimdiff




https://github.com/labstack/echo/issues/1337


Category golang