xgfone / ship

A flexible, powerful, high performance and minimalist Go Web HTTP router framework.
https://github.com/xgfone/ship
Apache License 2.0
48 stars 5 forks source link

请求body为formdata,对应接收结构体存在嵌套继承时,参数绑定不上 #4

Closed xmx closed 2 years ago

xmx commented 2 years ago

go version:1.17.3 ship version:5.1.0

复现代码:

type person struct {
    Name string `json:"name" form:"name"`
    Age  int    `json:"age"  form:"age"`
}

type Student struct {
    person        // 这个结构体必须是首字母小写
    School string `json:"school" form:"school"`
    Grade  int    `json:"grade"  form:"grade"`
}

func Demo(c *ship.Context) error {
    var stu Student
    if err := c.Bind(&stu); err != nil {
        return ship.ErrBadRequest.New(err)
    }

    fmt.Println(stu.Name)   // 参数没有绑定上
    fmt.Println(stu.Age)    // 参数没有绑定上
    fmt.Println(stu.School) // OK
    fmt.Println(stu.Grade)  // OK

    return c.JSON(http.StatusOK, stu)
}

func main() {
    router := ship.Default()
    router.Route("/demo").POST(Demo)
    ship.StartServer(":9999", router)

    // $ go run main.go
    //
    // 请求 body 格式是 FormData:
    // $ curl -X POST -F "name=小明" -F "age=9" -F "school=新民小学" -F "grade=3" http://localhost:9999/demo
    //
    // 响应结果:
    // {"name":"","age":0,"school":"新民小学","grade":3}
}
xgfone commented 2 years ago

这是 Go 匿名嵌套 struct 用法错误。

type Student struct {
    person        // 这个结构体必须是首字母小写
    School string `json:"school" form:"school"`
    Grade  int    `json:"grade"  form:"grade"`
}

_这种用法,属于匿名嵌套 struct,对于匿名嵌套,内层struct 仍是 外层 struct 的一个 Field,既然是 Field,那么它就有一个名字,该名字就是 内层 struct 类型的名字;对于上面的代码而言,就是 person。根据 Go语言规范,只有名字的首字母是大写的才能对外可见;对于上面的代码而言,名为 personField 是对外不可见的,因此,Bind 方法无法访问它,也就无法更新它的值,Bind 对其也就不成功(不是 Bind 有问题,而是 Go 语言规范或编译器所限制的)。_ 描述有误,不删除,作为记录。

解决这个问题有以下几个方法:

1、将 person 类型的名字改为 Person

type Person struct {
    Name string `json:"name" form:"name"`
    Age  int    `json:"age"  form:"age"`
}

type Student struct {
    Person           `json:"person"`
    School string   `json:"school" form:"school"`
    Grade  int      `json:"grade"  form:"grade"`
}

此时,匿名结构体 Field 的名字就变成 Person 了,因此,可以 Bind 它的值了。

2、明确写出 Field 的名字

如果结构体的类型名必须是小写字母开头,可以明确写出 Field 的名字,以代替匿名结构体,如下:

type person struct {
    Name string `json:"name" form:"name"`
    Age  int    `json:"age"  form:"age"`
}

type Student struct {
    Person persion  `json:"person"`
    School string `json:"school" form:"school"`
    Grade  int    `json:"grade"  form:"grade"`
}

其它说明: 如果既要内嵌结构体的类型名以小写字母开头,又要必须使用匿名结构体方式,那么这个匿名结构体 Field 就只能在当前包中才能访问,对于 Bind 功能,就要自己手动实现。

xgfone commented 2 years ago

上面的解释不周,对于 匿名嵌套 struct 的名字可见性 的描述部分有误,正确的是:当匿名 struct 自身的 Field 对外是可见时,不管匿名 struct 的类型自身是否是对外可见的,其对外可见的 Field 会自动变相地成为外层 struct 的对外可见的 Field

对于 Form 类型的 Bind,稍后改进,支持上述场景。

xmx commented 2 years ago

gingofiber 是可以正确的绑定成功

xgfone commented 2 years ago

gingofiber 是可以正确的绑定成功

是的,默认的 Form Bind实现(BindURLValues) 在处理上述情况下,仅判断了匿名嵌套结构体的可赋值性(即 reflect.Value.CanSet() ),未检查匿名嵌套结构体内部 Field 的可赋值性,所以才导致 Bind 未生效。这个稍后会修复。

xgfone commented 2 years ago

@xmx 已经修复。更新 版本到 v5.1.1 即可。如下:

// go.mod
module mypackage

require github.com/xgfone/ship/v5 v5.1.1

go 1.11
// main.go
package main

import (
    "fmt"
    "net/http"

    "github.com/xgfone/ship/v5"
)

type person struct {
    Name string `json:"name" form:"name"`
    Age  int    `json:"age"  form:"age"`
}

type Student struct {
    person        // 这个结构体必须是首字母小写
    School string `json:"school" form:"school"`
    Grade  int    `json:"grade"  form:"grade"`
}

func Demo(c *ship.Context) error {
    var stu Student
    if err := c.Bind(&stu); err != nil {
        return ship.ErrBadRequest.New(err)
    }

    fmt.Println(stu.Name)   // OK
    fmt.Println(stu.Age)    // OK
    fmt.Println(stu.School) // OK
    fmt.Println(stu.Grade)  // OK

    return c.JSON(http.StatusOK, stu)
}

func main() {
    router := ship.Default()
    router.Route("/demo").POST(Demo)
    ship.StartServer(":9999", router)

    // $ go run main.go
    //
    // 请求 body 格式是 FormData:
    // $ curl -X POST -F "name=小明" -F "age=9" -F "school=新民小学" -F "grade=3" http://localhost:9999/demo
    //
    // 响应结果:
    // {"name":"小明","age":9,"school":"新民小学","grade":3}
}
xmx commented 2 years ago

速度!