Go 语言获取 HTTP 请求数据(下):文件上传处理

表单数据编码类型

默认情况下,POST 表单编码类型属性 enctype 值是 application/x-www-form-urlencoded,其含义是将表单请求数据编码为 URL 参数,该属性用于指定提交表单时生成请求的请求头 Content-Type 的值。

还是以之前的在线论坛项目为例,在登录页面,点击「登录」按钮,通过 F12 查看对 authenticate 端点的网络请求明细:

表单请求Content-Type

可以看到 Content-Type 请求头的值确实是 application/x-www-form-urlencoded,虽然我们没有在 HTML 表单中设置 enctype 这个属性,因此它是默认值,你也可以显式设置表单元素的这个属性:

设置表单元素 enctype 属性

通过 application/x-www-form-urlencoded 编码的数据以 & 分隔的多个键值对, 同时以 = 分隔键和值,正如 URL 参数一样(查询字符串),非字母或数字的字符会被 Percent-encoding(通过百分号编码),还是以上面的登录请求表单数据为例:

查看表单请求数据

点击「view source」查看未经控制台工具解析的原生数据,编码后的真实数据如下所示:

查看表单请求数据原始值

因此,application/x-www-form-urlencoded 仅限于文本字符类数据编码,不能用于二进制数据编码,而通过表单上传的文件是以二进制流的方式提交到服务器的,因此不能通过默认的编码格式进行进行编码,需要通过专门的 multipart/form-data 编码类型。这种编码类型同时支持文本字符和二进制文件,在具体编码时,会将表单数据分成多个部分,每个文件单独占用一个部分,表单正文中包含的文本数据占用一个部分。

以学院君网站编辑个人资料页面为例:

包含文件上传的表单

这里的表单中包含了普通文本信息,也包含了文件上传(头像是图片文件),因此,表单的 enctype 类型设置成了 multipart/form-data。感兴趣的同学可以去看下表单提交过程中对应的请求头和请求实体信息,这里就不演示了。

MultipartForm

Go 语言为文件类型请求数据提供了单独的请求字段 MultipartForm,它是一个 multipart.Form 类型的指针,要解析并获取这个字段,可以这么做:

func EditPost(w http.ResponseWriter, r *http.Request)  {
    ...

    r.ParseMultipartForm(1024)
    fmt.Println("post file:", r.MultipartForm)

    io.WriteString(w, "表单提交成功")
}

这里,需要在调用 ParseMultipartForm 时传入存储解析后文件的最大内存值(单位是字节)。MultipartForm 包含了所有 POST 表单请求字段,即 PostForm 中的所有内容,但不包含 URL 查询字符串中的请求参数。MultipartForm 返回的值包含两个部分,一部分是单纯的 POST 请求字段,我们可以通过 Value 字段来访问它,另一部分就是包含文件信息的字典,我么可以通过 File 字段来访问它。

为了验证这个结论,我们重启 HTTP 服务器,打开 Postman 模拟客户端请求,填写 URL 和 表单字段(数据编码类型选择 form-data,即 multipart/form-data):

Postman模拟表单提交

表单数据设置好了之后,勾选上所有数据,然后点击「Send」发送请求,看到响应实体(Body)中显示「表单提交成功」,表明服务端已经处理完请求并成功返回响应,我们到启动 HTTP 服务器的位置查看服务端日志:

服务端查看表单请求数据

可以看到请求头中的 Content-Typemultipart/form-data,并且通过 r.MultipartForm 成功获取到了 POST 表单数据,包含文件信息(位于一个独立的 map 中)。

文件上传功能实现

文件解析和读取

接下来,我们根据前面学习到的知识点通过 Go 语言实现简单的文件上传功能。表单提交还是在 Postman 中模拟,在 handlers/post.go 中新增一个 UploadImage 处理器方法:

func UploadImage(w http.ResponseWriter, r *http.Request)  {
    r.ParseMultipartForm(1024 * 1000)  // 最大支持 1024 KB,即 1 M
    name := r.MultipartForm.Value["name"]  // 文件名
    image := r.MultipartForm.File["image"]  // 图片文件

    fmt.Println("图片上传成功: ", name[0])

    file, err := image[0].Open()
    if err == nil {
        data, err := ioutil.ReadAll(file)  // 读取二进制文件字节流
        if err == nil {
            fmt.Fprintln(w, string(data))   // 将读取的字节信息输出
        }
    }
}

在这段代码中,读取到文件数据后,将其赋值给 image,注意此时 image 是一个 FileHeader 指针数组,也就是说,通过一个字段名可以持有多个文件对象,这里只上传一张图片,那就是数组中的第一个对象,调用 FileHeaderOpen 方法打开字节流,再调用 ioutil.ReadAll 读取,最后将结果以字符串格式输出到响应实体,如果图片上传并读取成功,最终我们会在响应中看到这张上传的图片。

接下来,在 routes/web.go 中新增一个路由:

WebRoute{
    "UploadImage",
    "POST",
    "/image/upload",
    handlers.UploadImage,
},

重启 HTTP 服务器,在 Postman 中测试图片上传,在响应实体中看到上传的图片,则表示图片上传成功:

Go 语言文件上传处理

保存上传文件

当然,你还可以将其保存到服务器指定目录:

func UploadImage(w http.ResponseWriter, r *http.Request)  {
    r.ParseMultipartForm(1024 * 1000)  // 最大支持 1024 KB,即 1 M
    name := r.MultipartForm.Value["name"][0]  // 文件名
    image := r.MultipartForm.File["image"][0]  // 图片文件
    file, err := image.Open()
    if err == nil {
        data, err := ioutil.ReadAll(file)  // 读取二进制文件字节流
        if err == nil {
            // fmt.Fprintln(w, string(data))   // 将读取的字节信息输出
            // 将文件存储到项目根目录下的 images 子目录
            // 从上传文件中读取文件名并获取文件后缀
            names := strings.Split(image.Filename, ".")
            suffix := names[len(names) - 1]
            // 将上传文件名字段值和源文件后缀拼接出新的文件名
            filename := name + "." + suffix
            // 创建这个文件
            newFile, _ := os.Create("images/" + filename)
            defer newFile.Close()
            // 将上传文件的二进制字节信息写入新建的文件
            size, err := newFile.Write(data)
            if err == nil {
                fmt.Fprintf(w, "图片上传成功,图片大小: %d 字节\n", size / 1000)
            }
        }
    }
    if (err != nil) {
        fmt.Fprintln(w, err)
    }
}

相关逻辑在代码中已经详细注释了,不再赘述,在 goblog 目录下新增一个 images 子目录,重启 HTTP 服务器,再次在 Postman 中模拟表单上传图片:

Go 语言文件上传

此时到项目根目录下查看,对应的图片文件已存在:

服务端保存的上传文件

此外,和 FormValuePostFormValue 类似,还有一个 FormFile 用于快速获取上传文件:

file, _, err := r.FormFile("image")

这样一来,就需要再额外运行 r.ParseMultipartForm 解析文件数据了,底层会自动处理,并且默认的内存最大值是 32 MB。

多文件上传处理

感兴趣的同学还可以探索下多文件上传的实现。其实也很简单,比如这里示例下,将 UploadImage 中获取的图片调整为第二个:

func UploadImage(w http.ResponseWriter, r *http.Request)  {
    r.ParseMultipartForm(1024 * 1000)  // 最大支持 1024 KB,即 1 M
    name := r.MultipartForm.Value["name"][0]  // 文件名
    image := r.MultipartForm.File["image"][1]  // 图片文件
    ...
}

重启 HTTP 服务器,重新上传文件:

测试多文件上传

就可以在服务端看到这个新上传的文件了:

服务端保存的上传文件

如果要批量上传并保存多个文件,加入一层循环即可。

好了,关于文件上传及获取学院君就简单介绍到这里,下篇教程,将继续给大家介绍 Go Web 编程中的 HTTP 响应信息获取。

上一篇: Go 语言获取 HTTP 请求数据(上):查询字符串、表单请求和 JSON 请求

下一篇: Go 语言通过 ResponseWriter 对象发送 HTTP 响应