如何在 Go Gin 多部分请求中解析结构片

How to parse slice of structs in Go Gin multipart request

提问人:Rafał Mańka 提问时间:10/3/2023 最后编辑:Rafał Mańka 更新时间:10/6/2023 访问量:112

问:

这是我的应用程序:

func main() {
    router := gin.Default()
    router.POST("user", func(c *gin.Context) {
        type address struct {
            City    string `form:"city"`
            Country string `form:"country"`
        }
        type user struct {
            Name      string    `form:"name"`
            Addresses []address `form:"addresses"`
        }

        var payload user
        err := c.Bind(&payload)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{})
            return
        }

        file, err := c.FormFile("image")
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{})
            return
        }

        fmt.Printf("TODO: save user image %s\n", file.Filename)
        fmt.Printf("TODO: save user %s with addresses %d\n", payload.Name, len(payload.Addresses))
    })
    router.Run(":8080")
}

这是我的要求:

curl --location 'http://localhost:8080/user' \
--form 'name="John Smith"' \
--form 'addresses[0][city]="London"' \
--form 'addresses[0][country]="United Kingdom"' \
--form 'image=@"/Users/me/Documents/app/fixtures/user.jpeg"'

问题是地址为零。我无法找到有关如何使用嵌入式切片数据正确反序列化多部分请求的示例。我做错了什么?


更新:

我从 Postman 导出了 curl 命令。显然,如果您有“--form”参数,则 POST 方法是隐式的。以下是我的应用程序中的日志:

TODO: save user image user.jpeg
TODO: save user John Smith with addresses 0

更新2:

事实证明,像这样发送数组是错误的:

--form 'addresses[0][city]=“伦敦”'

gin 默认支持发送多个同名字段,然后这些字段将转换为数组:

--form 'addresses=... --form 'addresses=...

因此,这导致我找到了一个有点笨拙的解决方案,即将每个地址作为查询参数传递,然后单独解析它们:

请求:

curl --location 'http://localhost:8080/user' \
--form 'name="John Smith"' \
--form 'addresses="?city=London&country=UK"' \
--form 'addresses="?city=Berlin&country=Germany"'
--form 'image=@"/Users/foo/bar/baz/user.jpeg"' \

该应用程序:

func main() {
    router := gin.Default()
    router.POST("user", func(c *gin.Context) {
        type address struct {
            City    string `form:"city"`
            Country string `form:"country"`
        }
        type user struct {
            Name      string   `form:"name"`
            Addresses []string `form:"addresses"` // collection_format:"multi"
        }

        var payload user
        err := c.Bind(&payload)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{})
            return
        }

        var addresses []address
        for _, path := range payload.Addresses {
            parsed, err := url.Parse(path)
            if err != nil {
                continue
            }
            query := parsed.Query()
            addresses = append(addresses, address{
                City:    query.Get("city"),
                Country: query.Get("country"),
            })
        }

        file, err := c.FormFile("image")
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{})
            return
        }

        fmt.Printf("TODO: save user image %s\n", file.Filename)
        fmt.Printf("TODO: save user %s with addresses %d\n", payload.Name, len(addresses))

    })
    router.Run(":8080")
}

所以,如果有人有更好的解决方案,请告诉我。

Go Slice 多部分 go-gin

评论

0赞 Victor Vieira 10/3/2023
我的建议是在服务器端检查请求正文是否为预期的格式。尝试直接从窗体中读取该值,以确认问题出在分析器上。从截取的代码来看,到目前为止我没有看到任何问题。此外,您指向的请求是 GET。您的终结点假定为 POST。
1赞 Rafał Mańka 10/3/2023
@VictorVieira我更新了我的问题,我正在使用调试器捕获请求。Name 和 image 文件传递得刚刚好,但数组为 nil。

答:

1赞 Zargham Ahmed 10/6/2023 #1

一种解决方案是,不要将地址定义为字符串切片,而是将其定义为地址切片,并将每个地址作为 json 而不是查询参数传递。

请求-

curl --location 'http://localhost:8080/getForm' \
--form 'name ="Zargam"' \
--form 'addresses="{\"city\":\"Delhi\",\"country\":\"India\"}"' \
--form 'addresses="{\"city\":\"Shanghai\",\"country\":\"China\"}"'

法典-

package main

import (
    "encoding/json"
    "net/http"

    "github.com/gin-gonic/gin"
)

type address struct {
    City    string `form:"city"`
    Country string `form:"country"`
}
type user struct {
    Name      string    `form:"name"`
    Addresses []address `form:"addresses"` // collection_format:"multi"
}

func processForm(c *gin.Context) {
    var payload user
    err := c.Bind(&payload)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{})
        return
    }
    payloadByte, _ := json.Marshal(payload)

    c.JSON(http.StatusOK, gin.H{"message": "success", "data": string(payloadByte)})
    return
}

func main() {
    r := gin.Default()
    r.POST("/getForm", processForm)
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

评论

0赞 Rafał Mańka 10/7/2023
这个解决方案非常有效