我有这个请求处理程序的代码:

func (h *Handlers) UpdateProfile() gin.HandlerFunc {
    type request struct {
        Username    string `json:"username" binding:"required,min=4,max=20"`
        Description string `json:"description" binding:"required,max=100"`
    }

    return func(c *gin.Context) {
        var updateRequest request

        if err := c.BindJSON(&updateRequest); err != nil {
            var validationErrors validator.ValidationErrors

            if errors.As(err, &validationErrors) {
                validateErrors := base.BindingError(validationErrors)
                c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": validateErrors})
            } else {
                c.AbortWithError(http.StatusBadRequest, err)
            }

            return
        }

        avatar, err := c.FormFile("avatar")
        if err != nil {
            c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
                "error": "image not contains in request",
            })
            return
        }

        log.Print(avatar)

        if avatar.Size > 3<<20 { // if avatar size more than 3mb
            c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
                "error": "image is too large",
            })
            return
        }

        file, err := avatar.Open()
        if err != nil {
            c.AbortWithError(http.StatusInternalServerError, err)
        }

        session := sessions.Default(c)
        id := session.Get("sessionId")
        log.Printf("ID type: %T", id)

        err = h.userService.UpdateProfile(fmt.Sprintf("%v", id), file, updateRequest.Username, updateRequest.Description)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{})
            return
        }

        c.IndentedJSON(http.StatusNoContent, gin.H{"message": "succesfull update"})
    }
}

我有这个处理程序的单元测试:

func TestUser_UpdateProfile(t *testing.T) {
    type testCase struct {
        name               string
        image              io.Reader
        username           string
        description        string
        expectedStatusCode int
    }

    router := gin.Default()

    memStore := memstore.NewStore([]byte("secret"))
    router.Use(sessions.Sessions("session", memStore))

    userGroup := router.Group("user")
    repo := user.NewMemory()
    service := userService.New(repo)
    userHandlers.Register(userGroup, service)

    testImage := make([]byte, 100)
    rand.Read(testImage)
    image := bytes.NewReader(testImage)

    testCases := []testCase{
        {
            name:               "Request With Image",
            image:              image,
            username:           "bobik",
            description:        "wanna be sharik",
            expectedStatusCode: http.StatusNoContent,
        },
        {
            name:               "Request Without Image",
            image:              nil,
            username:           "sharik",
            description:        "wanna be bobik",
            expectedStatusCode: http.StatusNoContent,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            body := &bytes.Buffer{}
            writer := multipart.NewWriter(body)

            imageWriter, err := writer.CreateFormFile("avatar", "test_avatar.jpg")
            if err != nil {
                t.Fatal(err)
            }

            if _, err := io.Copy(imageWriter, image); err != nil {
                t.Fatal(err)
            }

            data := map[string]interface{}{
                "username":    tc.username,
                "description": tc.description,
            }
            jsonData, err := json.Marshal(data)
            if err != nil {
                t.Fatal(err)
            }

            jsonWriter, err := writer.CreateFormField("json")
            if err != nil {
                t.Fatal(err)
            }

            if _, err := jsonWriter.Write(jsonData); err != nil {
                t.Fatal(err)
            }

            writer.Close()

            // Creating request
            req := httptest.NewRequest(
                http.MethodPost,
                "http://localhost:8080/user/account/updateprofile",
                body,
            )
            req.Header.Set("Content-Type", writer.FormDataContentType())
            log.Print(req)

            w := httptest.NewRecorder()
            router.ServeHTTP(w, req)

            assert.Equal(t, tc.expectedStatusCode, w.Result().StatusCode)
        })
    }
}

在测试过程中,我遇到以下错误: 错误#01:数字文本中的字符‘-’无效

下面是请求正文(我用log.Print(Req)打印它):

&{POST http://localhost:8080/user/account/updateprofile HTTP/1.1 1 1 map[Content-Type:[multipart/form-data; boundary=30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035]] {--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name="avatar"; filename="test_avatar.jpg"
Content-Type: application/octet-stream


--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035
Content-Disposition: form-data; name="json"

{"description":"wanna be bobik","username":"sharik"}
--30b24345de9d8d83ecbdd146262d86894c45b4f3485e4615553621fd2035--
} <nil> 414 [] false localhost:8080 map[] map[] <nil> map[] 192.0.2.1:1234 http://localhost:8080/user/account/updateprofile <nil> <nil> <nil> <nil>}

首先,我只将字符串作为json数据,并将其转换为字节.当出现错误时,我使用json.Marshal转换json数据,但它不起作用.我想用c.Bind解析json数据,用c.FormFile解析给定的图像,可以吗?

UPD.我先替换代码以获取头像,然后通过绑定 struct 获取json.现在我有了EOF错误.

推荐答案

谢谢你@ossan!我还有更多的东西要分享.

1. Plain form data in multipart/form-data request works out of the box

顾名思义,multipart/form-data擅长处理表单数据.而且大多数浏览器和服务器都很好地支持它.

例如,以下页面的普通表单提交将以multipart/form-data为单位发送数据.

<html>
  <body>
    <form method="post" enctype="multipart/form-data" action="https://httpbin.org/post">
      <input type="text" name="username" />
      <input type="text" name="description" />
      <input type="file" name="avatar" />
      <button type="submit">submit</button>
    </form>
  </body>
</html>

至于GIN框架,这是开箱即用的支持(只需调用c.Bind(&updateRequest)):

type request struct {
    Avatar      *multipart.FileHeader `form:"avatar" binding:"required"`
    Username    string                `form:"username" binding:"required,min=4,max=20"`
    Description string                `form:"description" binding:"required,max=100"`
}
var updateRequest request
// c.Bind chooses formMultipartBinding based on the Content-Type header
if err := c.Bind(&updateRequest); err != nil {
    panic(err)
}

对于Go客户端,向请求添加更多表单数据字段如下所示:

writer.WriteField("username", tc.username)
writer.WriteField("description", tc.description)

2.纯格式数据的缺点

大多数情况下,纯格式数据会占用更多空间.请看这个简单的例子:

Content-Disposition: form-data; name="username"


--f4a39de2754ac499f99cc4852009b3bbd9f9890858fdd709026a0afea6ca
Content-Disposition: form-data; name="description"

wanna be sharik
--f4a39de2754ac499f99cc4852009b3bbd9f9890858fdd709026a0afea6ca

而纯格式数据不擅长携带复杂的数据.

3. Recieving json data from multipart/form-data request

我们可以将复杂数据编码为json数据并将其放入表单域.本质上,它是一个包含json数据的表单域.我们可以定义一个 struct 来同时接收json数据和文件(请注意字段标签):

type request struct {
    Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
    User   struct {
        Username    string `json:"username" binding:"required,min=4,max=20"`
        Description string `json:"description" binding:"required,max=100"`
    } `form:"user" binding:"required"`
}
var updateRequest request
// c.ShouldBind chooses formMultipartBinding based on the Content-Type header
if err := c.ShouldBind(&updateRequest); err != nil {
    c.AbortWithError(http.StatusBadRequest, err)
    return
}

4.MISC

  1. 不能使用c.BindJSONmultipart/form-data读取json,因为它假定请求正文以有效的json开始.但它是从一条边界开始的,看起来像--30b24345d....这就是它失败的原因,错误消息为invalid character '-' in numeric literal.
  2. c.FormFile("avatar")之后调用c.BindJSON不起作用,因为调用c.FormFile会读取整个请求正文.c.BindJSON之后就没有什么可读的了.这就是您看到EOF错误的原因.

5.单个可运行文件中的演示

这是完整的演示.以go test ./... -v -count 1分运行:

package m

import (
    "bytes"
    "crypto/rand"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gin-gonic/gin"
    "github.com/stretchr/testify/assert"
)

func handle(c *gin.Context) {
    type request struct {
        Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
        User   struct {
            Username    string `json:"username" binding:"required,min=4,max=20"`
            Description string `json:"description" binding:"required,max=100"`
        } `form:"user" binding:"required"`
    }
    var updateRequest request
    // c.ShouldBind chooses formMultipartBinding based on the Content-Type header
    if err := c.ShouldBind(&updateRequest); err != nil {
        c.AbortWithError(http.StatusBadRequest, err)
        return
    }

    fmt.Printf("%#v\n", updateRequest)

    c.IndentedJSON(http.StatusNoContent, gin.H{"message": "succesfull update"})
}

func TestUser_UpdateProfile(t *testing.T) {
    type testCase struct {
        name               string
        image              io.Reader
        username           string
        description        string
        expectedStatusCode int
    }

    router := gin.Default()

    router.POST("/update", handle)

    testImage := make([]byte, 100)
    rand.Read(testImage)
    image := bytes.NewReader(testImage)

    testCases := []testCase{
        {
            name:               "Request With Image",
            image:              image,
            username:           "bobik",
            description:        "wanna be sharik",
            expectedStatusCode: http.StatusNoContent,
        },
        {
            name:               "Request Without Image",
            image:              nil,
            username:           "sharik",
            description:        "wanna be bobik",
            expectedStatusCode: http.StatusBadRequest,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            body := &bytes.Buffer{}
            writer := multipart.NewWriter(body)

            if tc.image != nil {
                imageWriter, err := writer.CreateFormFile("avatar", "test_avatar.jpg")
                if err != nil {
                    t.Fatal(err)
                }

                if _, err := io.Copy(imageWriter, image); err != nil {
                    t.Fatal(err)
                }

            }

            data := map[string]interface{}{
                "username":    tc.username,
                "description": tc.description,
            }
            jsonData, err := json.Marshal(data)
            if err != nil {
                t.Fatal(err)
            }

            if err := writer.WriteField("user", string(jsonData)); err != nil {
                t.Fatal(err)
            }

            writer.Close()

            req := httptest.NewRequest(
                http.MethodPost,
                "http://localhost:8080/update",
                body,
            )
            req.Header.Set("Content-Type", writer.FormDataContentType())

            w := httptest.NewRecorder()
            router.ServeHTTP(w, req)

            assert.Equal(t, tc.expectedStatusCode, w.Result().StatusCode)
        })
    }
}

感谢您的阅读!

Json相关问答推荐

当有2个嵌套数组时展平复杂的JSON

Pandas 对REST API的自定义响应

如何在Swift中使用JSON编码器的泛型

将PNG图像保存为Python中的JSON文件

在深度嵌套数组中使用布尔属性的jq-select

当由.sh脚本执行时,AWS查询字符串不会提取任何数据

(已回答)JSON 读取函数返回未定义而不是预期值 - Typescript

使用 jq 获取所有嵌套键和值

将=分隔值文件转换为:json文件

在 PostgreSQL 中 Select 分层 JSON 作为表

是否可以在有条件的情况下将 json 对象转换为 JOLT 中的数组?

使用 KQL 和外部 data() 运算符从 json 文件中提取信息

如何在 Eclipse 中安装和使用 JSON 编辑器?

苗条的 JSON 输出

将带有数据和文件的 JSON 发布到 Web Api - jQuery / MVC

在 Jersey 服务中使用 JSON 对象

Json.NET 是否缓存类型的序列化信息?

如何转换为 D3 的 JSON 格式?

是否有一个 PHP 函数只在双引号而不是单引号中添加斜杠

JSON键是否需要唯一?