Go mime/multipart 包详解
概述
mime/multipart 包实现了 MIME multipart(多部分)消息的解析和生成,定义于 RFC 2046。该实现足够处理 HTTP(RFC 2388)以及流行浏览器生成的 multipart 消息体。包中提供了 Reader 和 Writer 两种主要类型,分别用于解析和生成 multipart 消息。
重要说明:
- ✓ 支持 MIME multipart 解析和生成(RFC 2046)
- ✓ 支持 HTTP 表单数据(RFC 2388)
- ✓ 自动处理 quoted-printable 编码
- ✓ 支持内存和磁盘存储大文件
- ✓ 内置安全限制防止恶意输入
- ✓ Go 1.0+ 引入,持续增强中
安全限制:
- 每个 part 的头部数量限制:10000(可通过
GODEBUG=multipartmaxheaders=<value>调整) - Form 中所有 FileHeader 的总头部数限制:10000
- Form 中 part 的数量限制:1000(可通过
GODEBUG=multipartmaxparts=<value>调整)
包导入
import (
"mime/multipart"
)
基本使用
1. 解析 multipart 消息
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
// 模拟 multipart 消息
body := `--boundary123
Content-Disposition: form-data; name="field1"
value1
--boundary123
Content-Disposition: form-data; name="field2"
value2
--boundary123--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary123")
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
fmt.Printf("Part name: %s\n", part.FormName())
data, _ := io.ReadAll(part)
fmt.Printf("Data: %s\n\n", string(data))
}
}
运行结果:
Part name: field1
Data: value1
Part name: field2
Data: value2
2. 生成 multipart 消息
package main
import (
"bytes"
"fmt"
"mime/multipart"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加普通字段
writer.WriteField("username", "john")
writer.WriteField("email", "john@example.com")
// 添加文件
fileWriter, _ := writer.CreateFormFile("avatar", "photo.jpg")
fileWriter.Write([]byte("fake image data"))
writer.Close()
fmt.Printf("Content-Type: %s\n", writer.FormDataContentType())
fmt.Printf("Body length: %d bytes\n", buf.Len())
}
运行结果:
Content-Type: multipart/form-data; boundary=30405f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b
Body length: 378 bytes
3. 处理文件上传
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
// 模拟文件上传
body := `--boundary123
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
Hello, World!
--boundary123--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary123")
part, _ := reader.NextPart()
fmt.Printf("Field: %s\n", part.FormName())
fmt.Printf("Filename: %s\n", part.FileName())
data, _ := io.ReadAll(part)
fmt.Printf("Content: %s\n", string(data))
}
运行结果:
Field: file
Filename: test.txt
Content: Hello, World!
一、变量
ErrMessageTooLarge
定义:
var ErrMessageTooLarge = errors.New("multipart: message too large")
说明:
- 功能:当 multipart 消息太大无法处理时返回的错误
- 触发条件:ReadForm 处理的消息超过内存限制
- 用途:用于错误处理和判断消息大小
示例:
form, err := reader.ReadForm(32 << 20) // 32MB 限制
if err == multipart.ErrMessageTooLarge {
fmt.Println("消息太大,无法处理")
return
}
二、函数(按 a-z 排序)
FileContentDisposition
定义:
func FileContentDisposition(fieldname, filename string) string
说明:
- 功能:生成 Content-Disposition 头部值
- 参数:
fieldname- 字段名称filename- 文件名
- 返回:Content-Disposition 头部字符串
- 用途:用于设置文件上传字段的头部
- 版本:Go 1.25.0+ 引入
示例:
package main
import (
"fmt"
"mime/multipart"
)
func main() {
// 生成 Content-Disposition 头部
disposition := multipart.FileContentDisposition("avatar", "photo.jpg")
fmt.Println(disposition)
// 包含特殊字符的文件名
disposition = multipart.FileContentDisposition("document", "文档(测试).pdf")
fmt.Println(disposition)
}
运行结果:
form-data; name="avatar"; filename="photo.jpg"
form-data; name="document"; filename="文档(测试).pdf"
三、类型(按 a-z 排序)
File
定义:
type File interface {
io.Reader
io.ReaderAt
io.Seeker
io.Closer
}
说明:
- 功能:访问 multipart 消息中文件部分的接口
- 内容存储:可能在内存中,也可能在磁盘上
- 磁盘存储:如果存储在磁盘上,底层具体类型是
*os.File - 用途:用于处理上传的文件
示例:
package main
import (
"fmt"
"io"
"mime/multipart"
"os"
"strings"
)
func main() {
// 模拟文件上传
body := `--boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
Hello, World!
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
form, _ := reader.ReadForm(10 << 20)
// 获取文件头
fileHeaders := form.File["file"]
for _, fh := range fileHeaders {
// 打开文件
file, _ := fh.Open()
defer file.Close()
// 读取内容
data, _ := io.ReadAll(file)
fmt.Printf("File content: %s\n", string(data))
}
form.RemoveAll()
}
运行结果:
File content: Hello, World!
FileHeader
定义:
type FileHeader struct {
// 包含未导出的字段
}
说明:
- 功能:描述 multipart 请求中的文件部分
- 用途:包含文件的元数据(文件名、大小、头部等)
- 访问内容:通过
Open()方法访问文件内容
方法:
Open
定义:
func (fh *FileHeader) Open() (File, error)
说明:
- 功能:打开 FileHeader 关联的文件
- 返回:File 接口和错误信息
- 存储位置:文件可能在内存或磁盘临时文件中
示例:
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
body := `--boundary
Content-Disposition: form-data; name="upload"; filename="data.txt"
Content-Type: text/plain
File content here
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
form, _ := reader.ReadForm(10 << 20)
// 获取第一个文件头
if len(form.File["upload"]) > 0 {
fh := form.File["upload"][0]
// 打开文件
file, err := fh.Open()
if err != nil {
panic(err)
}
defer file.Close()
// 读取内容
content, _ := io.ReadAll(file)
fmt.Printf("Content: %s\n", string(content))
}
form.RemoveAll()
}
运行结果:
Content: File content here
Form
定义:
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
说明:
- 功能:解析后的 multipart 表单
- 字段:
Value- 普通字段的键值对(可能多个值)File- 文件字段的键值对(文件头切片)
- 存储方式:文件部分存储在内存或磁盘临时文件中
- 用途:处理 HTTP 表单提交
方法:
RemoveAll
定义:
func (f *Form) RemoveAll() error
说明:
- 功能:删除与 Form 关联的所有临时文件
- 返回:错误信息
- 用途:清理资源,应在处理完成后调用
- 注意:即使出错也应调用以清理资源
示例:
package main
import (
"fmt"
"mime/multipart"
"strings"
)
func main() {
body := `--boundary
Content-Disposition: form-data; name="text"
text value
--boundary
Content-Disposition: form-data; name="file"; filename="test.txt"
file content
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
form, err := reader.ReadForm(10 << 20)
if err != nil {
panic(err)
}
defer form.RemoveAll() // 确保清理临时文件
// 访问普通字段
fmt.Println("Text fields:", form.Value["text"])
// 访问文件字段
fmt.Println("File count:", len(form.File["file"]))
// 处理文件
for _, fh := range form.File["file"] {
fmt.Println("Filename:", fh.Filename)
}
}
运行结果:
Text fields: [text value]
File count: 1
Filename: test.txt
Part
定义:
type Part struct {
// 包含未导出的字段
}
说明:
- 功能:表示 multipart 消息体中的单个部分
- 用途:访问部分的头部和内容
- 特点:实现了
io.Reader接口
方法:
Close
定义:
func (p *Part) Close() error
说明:
- 功能:关闭 Part,释放相关资源
- 返回:错误信息
- 用途:在处理完 Part 后调用以清理资源
示例:
part, err := reader.NextPart()
if err != nil {
// 处理错误
}
defer part.Close() // 确保关闭
// 读取内容
data, _ := io.ReadAll(part)
FileName
定义:
func (p *Part) FileName() string
说明:
- 功能:返回 Part 的 Content-Disposition 头部中的 filename 参数
- 返回:文件名字符串
- 处理:如果非空,会通过
filepath.Base处理(平台相关) - 用途:获取上传文件的原始文件名
示例:
part, _ := reader.NextPart()
filename := part.FileName()
fmt.Printf("Uploaded file: %s\n", filename)
FormName
定义:
func (p *Part) FormName() string
说明:
- 功能:如果 Part 的 Content-Disposition 类型为 “form-data”,返回 name 参数
- 返回:字段名称,如果不是 form-data 则返回空字符串
- 用途:获取表单字段名
示例:
part, _ := reader.NextPart()
fieldName := part.FormName()
fmt.Printf("Field name: %s\n", fieldName)
Read
定义:
func (p *Part) Read(d []byte) (n int, err error)
说明:
- 功能:读取 Part 的正文内容
- 参数:
d- 目标字节切片
- 返回:
n- 读取的字节数err- 错误信息(io.EOF 表示结束)
- 特点:
- 读取 Part 头部之后、下一个 Part 之前的内容
- 如果 “Content-Transfer-Encoding” 为 “quoted-printable”,会自动解码
- 用途:读取部分的内容数据
示例:
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
body := `--boundary
Content-Disposition: form-data; name="data"
Hello, World!
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
part, _ := reader.NextPart()
// 读取内容
data, err := io.ReadAll(part)
if err != nil {
panic(err)
}
fmt.Printf("Content: %s\n", string(data))
part.Close()
}
运行结果:
Content: Hello, World!
Reader
定义:
type Reader struct {
// 包含未导出的字段
}
说明:
- 功能:MIME multipart 消息体的迭代器
- 特点:
- 按需解析输入内容
- 不支持查找(Seek)
- 自动处理 quoted-printable 编码
- 用途:解析 multipart 消息
方法:
NewReader
定义:
func NewReader(r io.Reader, boundary string) *Reader
说明:
- 功能:创建新的 multipart Reader
- 参数:
r- 输入流(io.Reader)boundary- MIME 边界字符串
- 返回:新的 Reader 指针
- 用途:初始化 Reader 以解析 multipart 消息
- 边界来源:通常从 “Content-Type” 头部的 “boundary” 参数获取
示例:
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
body := `--boundary123
Content-Disposition: form-data; name="one"
A section
--boundary123
Content-Disposition: form-data; name="two"
And another
--boundary123--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary123")
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
data, _ := io.ReadAll(part)
fmt.Printf("Part %q: %q\n", part.FormName(), string(data))
part.Close()
}
}
运行结果:
Part "one": "A section"
Part "two": "And another"
NextPart
定义:
func (r *Reader) NextPart() (*Part, error)
说明:
- 功能:返回 multipart 中的下一个 Part
- 返回:
*Part- 下一个部分的指针error- 错误信息(io.EOF 表示没有更多部分)
- 特殊处理:
- 如果 “Content-Transfer-Encoding” 为 “quoted-printable”,该头部会被隐藏
- 在 Read 调用期间自动解码 quoted-printable 内容
- 用途:迭代处理 multipart 的各个部分
示例:
reader := multipart.NewReader(request.Body, boundary)
for {
part, err := reader.NextPart()
if err == io.EOF {
break // 所有部分处理完毕
}
if err != nil {
// 处理错误
return err
}
// 处理当前部分
fieldName := part.FormName()
fileName := part.FileName()
if fileName != "" {
// 文件上传
handleFile(part, fileName)
} else {
// 普通字段
data, _ := io.ReadAll(part)
processField(fieldName, string(data))
}
part.Close()
}
NextRawPart
定义:
func (r *Reader) NextRawPart() (*Part, error)
说明:
- 功能:返回 multipart 中的下一个 Part(原始数据)
- 返回:
*Part- 下一个部分的指针error- 错误信息(io.EOF 表示没有更多部分)
- 与 NextPart 的区别:
- 不处理 “Content-Transfer-Encoding: quoted-printable”
- 返回原始编码的数据
- 用途:需要访问原始编码数据时使用
示例:
reader := multipart.NewReader(request.Body, boundary)
for {
part, err := reader.NextRawPart()
if err == io.EOF {
break
}
if err != nil {
return err
}
// 获取原始数据(不解码 quoted-printable)
data, _ := io.ReadAll(part)
fmt.Printf("Raw content: %x\n", data)
part.Close()
}
ReadForm
定义:
func (r *Reader) ReadForm(maxMemory int64) (*Form, error)
说明:
- 功能:解析整个 multipart 消息,所有部分的 Content-Disposition 为 “form-data”
- 参数:
maxMemory- 内存存储的最大字节数(额外保留 10MB 用于非文件部分)
- 返回:
*Form- 解析后的表单error- 错误信息(可能返回 ErrMessageTooLarge)
- 存储策略:
- 不超过 maxMemory + 10MB 的部分存储在内存
- 超出部分存储在磁盘临时文件中
- 用途:处理 HTTP 表单提交
示例:
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func main() {
body := `--boundary
Content-Disposition: form-data; name="username"
john
--boundary
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
fake image data
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
// 解析表单,内存限制 32MB
form, err := reader.ReadForm(32 << 20)
if err != nil {
if err == multipart.ErrMessageTooLarge {
fmt.Println("消息太大")
return
}
panic(err)
}
defer form.RemoveAll()
// 访问普通字段
fmt.Println("Username:", form.Value["username"][0])
// 访问文件字段
if len(form.File["avatar"]) > 0 {
fh := form.File["avatar"][0]
fmt.Println("Avatar filename:", fh.Filename)
// 打开并读取文件
file, _ := fh.Open()
defer file.Close()
data, _ := io.ReadAll(file)
fmt.Printf("Avatar size: %d bytes\n", len(data))
}
}
运行结果:
Username: john
Avatar filename: photo.jpg
Avatar size: 15 bytes
内存限制说明:
maxMemory:用于存储文件部分的内存上限- 额外 10MB:保留用于非文件部分(普通字段)
- 超出部分:自动存储到磁盘临时文件
- 返回错误:如果所有非文件部分无法存储在内存中,返回 ErrMessageTooLarge
Writer
定义:
type Writer struct {
// 包含未导出的字段
}
说明:
- 功能:生成 multipart 消息
- 用途:创建 multipart/form-data 请求体
- 特点:自动生成随机边界字符串
方法:
NewWriter
定义:
func NewWriter(w io.Writer) *Writer
说明:
- 功能:创建新的 multipart Writer
- 参数:
w- 输出流(io.Writer)
- 返回:新的 Writer 指针
- 特点:自动生成随机边界字符串
- 用途:初始化 Writer 以生成 multipart 消息
示例:
package main
import (
"bytes"
"fmt"
"mime/multipart"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加字段
writer.WriteField("name", "Alice")
writer.Close()
fmt.Printf("Generated %d bytes\n", buf.Len())
fmt.Printf("Boundary: %s\n", writer.Boundary())
}
运行结果:
Generated 134 bytes
Boundary: 30405f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b
Boundary
定义:
func (w *Writer) Boundary() string
说明:
- 功能:返回 Writer 使用的边界字符串
- 返回:边界字符串
- 用途:用于设置 Content-Type 头部
示例:
writer := multipart.NewWriter(&buf)
boundary := writer.Boundary()
contentType := "multipart/form-data; boundary=" + boundary
Close
定义:
func (w *Writer) Close() error
说明:
- 功能:完成 multipart 消息,写入结束边界
- 返回:错误信息
- 用途:必须在所有部分写入后调用
- 注意:不调用 Close 会导致消息不完整
示例:
writer := multipart.NewWriter(&buf)
// 添加字段和文件
writer.WriteField("name", "Alice")
writer.WriteField("email", "alice@example.com")
// 必须调用 Close 完成消息
err := writer.Close()
if err != nil {
panic(err)
}
CreateFormField
定义:
func (w *Writer) CreateFormField(fieldname string) (io.Writer, error)
说明:
- 功能:创建新的表单字段部分
- 参数:
fieldname- 字段名称
- 返回:
io.Writer- 用于写入字段值的写入器error- 错误信息
- 用途:添加普通文本字段
- 实现:调用 CreatePart 创建带有适当头部的部分
示例:
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 创建字段
fieldWriter, err := writer.CreateFormField("username")
if err != nil {
panic(err)
}
// 写入值
io.WriteString(fieldWriter, "john_doe")
writer.Close()
fmt.Printf("Generated %d bytes\n", buf.Len())
}
运行结果:
Generated 140 bytes
CreateFormFile
定义:
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)
说明:
- 功能:创建新的文件表单字段
- 参数:
fieldname- 字段名称filename- 文件名
- 返回:
io.Writer- 用于写入文件内容的写入器error- 错误信息
- 用途:添加文件上传字段
- 实现:CreatePart 的便利封装,自动设置 Content-Disposition 和 Content-Type
示例:
package main
import (
"bytes"
"fmt"
"mime/multipart"
"os"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 创建文件字段
fileWriter, err := writer.CreateFormFile("avatar", "photo.jpg")
if err != nil {
panic(err)
}
// 写入文件内容
fileWriter.Write([]byte("fake image data"))
// 添加普通字段
writer.WriteField("username", "john")
writer.Close()
fmt.Printf("Generated %d bytes\n", buf.Len())
fmt.Printf("Content-Type: %s\n", writer.FormDataContentType())
}
运行结果:
Generated 378 bytes
Content-Type: multipart/form-data; boundary=30405f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b
自动设置 Content-Type:
- 根据文件扩展名自动检测 Content-Type
- 如果无法检测,使用
application/octet-stream
CreatePart
定义:
func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error)
说明:
- 功能:创建具有自定义头部的新 multipart 部分
- 参数:
header- MIME 头部
- 返回:
io.Writer- 用于写入部分内容的写入器error- 错误信息
- 用途:创建自定义部分(高级用法)
- 注意:调用 CreatePart 后,不能再写入之前的任何部分
示例:
package main
import (
"bytes"
"fmt"
"mime/multipart"
"mime/textproto"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 创建自定义头部
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="custom"`)
h.Set("Content-Type", "application/json")
// 创建部分
partWriter, err := writer.CreatePart(h)
if err != nil {
panic(err)
}
// 写入 JSON 数据
partWriter.Write([]byte(`{"key": "value"}`))
writer.Close()
fmt.Printf("Generated %d bytes\n", buf.Len())
}
运行结果:
Generated 224 bytes
FormDataContentType
定义:
func (w *Writer) Writer) FormDataContentType() string
说明:
- 功能:返回用于 HTTP multipart/form-data 的 Content-Type 值
- 返回:包含边界的 Content-Type 字符串
- 格式:
multipart/form-data; boundary=xxxxx - 用途:设置 HTTP 请求的 Content-Type 头部
示例:
package main
import (
"fmt"
"mime/multipart"
"net/http"
"bytes"
)
func uploadFile() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加文件
fileWriter, _ := writer.CreateFormFile("file", "test.txt")
fileWriter.Write([]byte("file content"))
writer.Close()
// 创建 HTTP 请求
req, _ := http.NewRequest("POST", "/upload", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
fmt.Println("Content-Type:", writer.FormDataContentType())
}
func main() {
uploadFile()
}
运行结果:
Content-Type: multipart/form-data; boundary=30405f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b3f3b
SetBoundary
定义:
func (w *Writer) SetBoundary(boundary string) error
说明:
- 功能:使用显式值覆盖自动生成的边界
- 参数:
boundary- 自定义边界字符串
- 返回:错误信息
- 限制:
- 必须在创建任何部分之前调用
- 只能包含某些 ASCII 字符
- 必须非空且最多 70 字节
- 用途:需要固定边界时使用(如测试)
示例:
package main
import (
"bytes"
"fmt"
"mime/multipart"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 设置自定义边界
err := writer.SetBoundary("MyCustomBoundary123")
if err != nil {
panic(err)
}
writer.WriteField("field", "value")
writer.Close()
fmt.Printf("Boundary: %s\n", writer.Boundary())
fmt.Printf("Generated:\n%s\n", buf.String())
}
运行结果:
Boundary: MyCustomBoundary123
Generated:
--MyCustomBoundary123
Content-Disposition: form-data; name="field"
value
--MyCustomBoundary123--
边界字符限制:
- 允许:字母、数字、标点符号
- 不允许:空格、控制字符
- 最大长度:70 字节
WriteField
定义:
func (w *Writer) WriteField(fieldname, value string) error
说明:
- 功能:添加普通表单字段
- 参数:
fieldname- 字段名称value- 字段值
- 返回:错误信息
- 用途:快速添加文本字段
- 实现:调用 CreateFormField 然后写入值
示例:
package main
import (
"bytes"
"fmt"
"mime/multipart"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加多个字段
writer.WriteField("username", "john_doe")
writer.WriteField("email", "john@example.com")
writer.WriteField("age", "25")
writer.Close()
fmt.Printf("Added %d fields\n", 3)
fmt.Printf("Generated %d bytes\n", buf.Len())
}
运行结果:
Added 3 fields
Generated 350 bytes
四、典型示例
示例 1:HTTP 文件上传处理
package main
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 限制内存使用 32MB
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 获取文件头
fileHeaders := r.MultipartForm.File["avatar"]
if len(fileHeaders) == 0 {
http.Error(w, "no file uploaded", http.StatusBadRequest)
return
}
fileHeader := fileHeaders[0]
// 打开上传的文件
file, err := fileHeader.Open()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
// 保存到磁盘
dst, err := os.Create("./uploads/" + fileHeader.Filename)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// 复制内容
_, err = io.Copy(dst, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "File uploaded successfully: %s", fileHeader.Filename)
}
func main() {
http.HandleFunc("/upload", uploadHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
示例 2:创建 multipart 请求
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func uploadFile(url, filePath string) error {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加普通字段
writer.WriteField("username", "john")
writer.WriteField("description", "My avatar")
// 打开本地文件
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// 创建文件字段
fileWriter, err := writer.CreateFormFile("avatar", "photo.jpg")
if err != nil {
return err
}
// 复制文件内容
_, err = io.Copy(fileWriter, file)
if err != nil {
return err
}
writer.Close()
// 创建请求
req, err := http.NewRequest("POST", url, &buf)
if err != nil {
return err
}
// 设置 Content-Type
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("Status: %s\n", resp.Status)
return nil
}
func main() {
err := uploadFile("http://example.com/upload", "./photo.jpg")
if err != nil {
panic(err)
}
}
示例 3:解析复杂 multipart 消息
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func parseMultipart() {
body := `--boundary123
Content-Disposition: form-data; name="text"
Plain text
--boundary123
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
File content
--boundary123
Content-Disposition: form-data; name="json"
Content-Type: application/json
{"key": "value"}
--boundary123--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary123")
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
fmt.Printf("=== Part ===\n")
fmt.Printf("Name: %s\n", part.FormName())
fmt.Printf("Filename: %s\n", part.FileName())
fmt.Printf("Content-Type: %s\n", part.Header.Get("Content-Type"))
data, _ := io.ReadAll(part)
fmt.Printf("Content: %s\n", string(data))
part.Close()
}
}
func main() {
parseMultipart()
}
运行结果:
=== Part ===
Name: text
Filename:
Content-Type:
Content: Plain text
=== Part ===
Name: file
Filename: test.txt
Content-Type: text/plain
Content: File content
=== Part ===
Name: json
Filename:
Content-Type: application/json
Content: {"key": "value"}
示例 4:批量文件上传
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func uploadMultipleFiles(url string, filePaths []string) error {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 添加多个文件
for i, filePath := range filePaths {
file, err := os.Open(filePath)
if err != nil {
return err
}
fileName := filepath.Base(filePath)
fileWriter, err := writer.CreateFormFile(fmt.Sprintf("file%d", i), fileName)
if err != nil {
file.Close()
return err
}
_, err = io.Copy(fileWriter, file)
file.Close()
if err != nil {
return err
}
}
// 添加元数据
writer.WriteField("count", fmt.Sprintf("%d", len(filePaths)))
writer.WriteField("batch", "true")
writer.Close()
// 发送请求
req, _ := http.NewRequest("POST", url, &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("Uploaded %d files, status: %s\n", len(filePaths), resp.Status)
return nil
}
func main() {
files := []string{"file1.txt", "file2.txt", "file3.txt"}
err := uploadMultipleFiles("http://example.com/upload", files)
if err != nil {
panic(err)
}
}
示例 5:自定义边界和头部
package main
import (
"bytes"
"fmt"
"mime/multipart"
"mime/textproto"
)
func customMultipart() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// 设置自定义边界
writer.SetBoundary("CustomBoundary123")
// 创建自定义部分
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="custom"`)
h.Set("Content-Type", "application/json")
h.Set("X-Custom-Header", "value")
part, _ := writer.CreatePart(h)
part.Write([]byte(`{"custom": "data"}`))
// 添加普通字段
writer.WriteField("normal", "field")
writer.Close()
fmt.Printf("Output:\n%s\n", buf.String())
}
func main() {
customMultipart()
}
运行结果:
Output:
--CustomBoundary123
Content-Disposition: form-data; name="custom"
Content-Type: application/json
X-Custom-Header: value
{"custom": "data"}
--CustomBoundary123
Content-Disposition: form-data; name="normal"
field
--CustomBoundary123--
示例 6:处理 quoted-printable 编码
package main
import (
"fmt"
"io"
"mime/multipart"
"strings"
)
func handleQuotedPrintable() {
// 包含 quoted-printable 编码的消息
body := `--boundary
Content-Disposition: form-data; name="encoded"
Content-Transfer-Encoding: quoted-printable
Hello=2C=20World=21
=C2=A1Hola=2C=20se=C3=B1or!
--boundary--
`
reader := multipart.NewReader(strings.NewReader(body), "boundary")
// 使用 NextPart 会自动解码
part, _ := reader.NextPart()
fmt.Printf("NextPart (decoded):\n")
data, _ := io.ReadAll(part)
fmt.Printf("%s\n\n", string(data))
part.Close()
// 使用 NextRawPart 获取原始数据
body2 := `--boundary
Content-Disposition: form-data; name="encoded"
Content-Transfer-Encoding: quoted-printable
Hello=2C=20World=21
--boundary--
`
reader2 := multipart.NewReader(strings.NewReader(body2), "boundary")
rawPart, _ := reader2.NextRawPart()
fmt.Printf("NextRawPart (raw):\n")
data, _ = io.ReadAll(rawPart)
fmt.Printf("%s\n", string(data))
rawPart.Close()
}
func main() {
handleQuotedPrintable()
}
运行结果:
NextPart (decoded):
Hello, World!
¡Hola, señor!
NextRawPart (raw):
Hello=2C=20World=21
示例 7:内存和磁盘存储
package main
import (
"fmt"
"io"
"mime/multipart"
"os"
"strings"
)
func memoryAndDiskStorage() {
// 模拟大文件上传
largeContent := strings.Repeat("x", 1024*1024) // 1MB 内容
body := fmt.Sprintf(`--boundary
Content-Disposition: form-data; name="small"
small value
--boundary
Content-Disposition: form-data; name="large"; filename="large.txt"
%s
--boundary--
`, largeContent)
reader := multipart.NewReader(strings.NewReader(body), "boundary")
// 内存限制很小,强制使用磁盘存储
form, err := reader.ReadForm(100) // 仅 100 字节内存
if err != nil {
panic(err)
}
defer form.RemoveAll()
// 小字段在内存
fmt.Printf("Small field: %s\n", form.Value["small"][0])
// 大文件在磁盘
if len(form.File["large"]) > 0 {
fh := form.File["large"][0]
fmt.Printf("Large file stored on disk\n")
file, _ := fh.Open()
defer file.Close()
// 检查是否是临时文件
if f, ok := file.(*os.File); ok {
fmt.Printf("File path: %s\n", f.Name())
}
// 读取部分内容
buf := make([]byte, 10)
n, _ := file.Read(buf)
fmt.Printf("Content preview: %s...\n", string(buf[:n]))
}
}
func main() {
memoryAndDiskStorage()
}
运行结果:
Small field: small value
Large file stored on disk
File path: /tmp/multipart-123456789
Content preview: xxxxxxxxxx...
示例 8:完整的文件上传服务器
package main
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
const uploadDir = "./uploads"
func init() {
os.MkdirAll(uploadDir, 0755)
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析 multipart 表单,内存限制 32MB
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var uploadedFiles []string
// 处理所有上传的文件
for fieldName, fileHeaders := range r.MultipartForm.File {
for _, fh := range fileHeaders {
// 验证文件类型
if !isValidFileType(fh.Filename) {
http.Error(w, "Invalid file type", http.StatusBadRequest)
return
}
// 打开上传的文件
src, err := fh.Open()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer src.Close()
// 创建目标文件
filename := filepath.Base(fh.Filename)
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// 复制内容
_, err = io.Copy(dst, src)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uploadedFiles = append(uploadedFiles, filename)
fmt.Printf("Received file: %s (field: %s, size: %d bytes)\n",
filename, fieldName, fh.Size)
}
}
// 返回结果
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"success": true, "files": %q}`, uploadedFiles)
}
func isValidFileType(filename string) bool {
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt"}
ext := strings.ToLower(filepath.Ext(filename))
for _, allowed := range allowedExts {
if ext == allowed {
return true
}
}
return false
}
func main() {
http.HandleFunc("/upload", uploadHandler)
fmt.Println("Upload server starting on :8080")
http.ListenAndServe(":8080", nil)
}
五、最佳实践
1. 始终调用 RemoveAll
// ✓ 正确:使用 defer 确保清理
form, err := reader.ReadForm(32 << 20)
if err != nil {
return err
}
defer form.RemoveAll() // 确保清理临时文件
// ✗ 错误:忘记清理
form, _ := reader.ReadForm(32 << 20)
// 处理文件...
// 临时文件未被清理!
2. 设置合理的内存限制
// ✓ 正确:根据预期文件大小设置
const maxMemory = 32 << 20 // 32MB
form, err := reader.ReadForm(maxMemory)
// ✗ 错误:限制过小或过大
form, err := reader.ReadForm(1024) // 太小,频繁磁盘 IO
form, err := reader.ReadForm(1024 << 20) // 太大,可能内存溢出
3. 验证文件类型
// ✓ 正确:验证文件扩展名和内容
func validateFile(fh *multipart.FileHeader) error {
ext := strings.ToLower(filepath.Ext(fh.Filename))
allowedExts := map[string]bool{
".jpg": true, ".png": true, ".pdf": true,
}
if !allowedExts[ext] {
return fmt.Errorf("invalid file type: %s", ext)
}
// 进一步验证文件内容(magic number)
file, _ := fh.Open()
defer file.Close()
buf := make([]byte, 512)
n, _ := file.Read(buf)
contentType := http.DetectContentType(buf[:n])
if !isAllowedContentType(contentType) {
return fmt.Errorf("invalid content type: %s", contentType)
}
return nil
}
// ✗ 错误:不验证文件类型
// 可能导致安全漏洞
4. 处理大文件
// ✓ 正确:流式处理大文件
func handleLargeFile(part *multipart.Part) error {
// 不要一次性读取到内存
// data, _ := io.ReadAll(part) // 可能内存溢出
// 使用流式处理
dst, _ := os.Create("./uploads/largefile")
defer dst.Close()
_, err := io.Copy(dst, part) // 流式复制
return err
}
// ✗ 错误:一次性读取大文件
data, _ := io.ReadAll(part) // 大文件会导致内存问题
5. 设置 Content-Type
// ✓ 正确:使用 FormDataContentType
writer := multipart.NewWriter(&buf)
// ... 添加字段和文件
writer.Close()
req, _ := http.NewRequest("POST", url, &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
// ✗ 错误:手动设置边界
req.Header.Set("Content-Type", "multipart/form-data") // 缺少 boundary
6. 错误处理
// ✓ 正确:完整的错误处理
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
log.Printf("Error reading part: %v", err)
return err
}
// 处理部分
if err := processPart(part); err != nil {
part.Close()
return err
}
part.Close()
}
// ✗ 错误:忽略错误
for {
part, _ := reader.NextPart()
// 没有检查 EOF 和其他错误
}
六、与其他包配合
1. 与 net/http 配合
import (
"mime/multipart"
"net/http"
)
// 服务器端处理上传
func handler(w http.ResponseWriter, r *http.Request) {
// ParseMultipartForm 调用 multipart.Reader.ReadForm
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 访问解析后的表单
values := r.MultipartForm.Value
files := r.MultipartForm.File
// 处理文件...
}
// 客户端发送上传
func upload() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
writer.WriteField("field", "value")
req, _ := http.NewRequest("POST", "/upload", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
client.Do(req)
}
2. 与 io 配合
import (
"io"
"mime/multipart"
)
// 流式复制文件
func copyFile(part *multipart.Part, dst io.Writer) error {
_, err := io.Copy(dst, part)
return err
}
// 读取到内存
func readToMemory(part *multipart.Part) ([]byte, error) {
return io.ReadAll(part)
}
// 限制读取大小
func readLimited(part *multipart.Part, maxBytes int64) ([]byte, error) {
return io.ReadAll(io.LimitReader(part, maxBytes))
}
3. 与 mime 配合
import (
"mime"
"mime/multipart"
)
// 从 Content-Type 获取边界
func getBoundary(contentType string) (string, error) {
_, params, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
return params["boundary"], nil
}
// 创建 Reader
contentType := "multipart/form-data; boundary=----WebKitFormBoundary"
_, params, _ := mime.ParseMediaType(contentType)
reader := multipart.NewReader(request.Body, params["boundary"])
七、快速参考
变量
| 变量 | 类型 | 说明 |
|---|---|---|
ErrMessageTooLarge | error | 消息太大无法处理 |
函数
| 函数 | 功能 | 返回值 |
|---|---|---|
FileContentDisposition(fieldname, filename) | 生成 Content-Disposition 头部 | string |
类型
| 类型 | 功能 |
|---|---|
File | 文件接口(io.Reader/ReaderAt/Seeker/Closer) |
FileHeader | 文件部分头部描述 |
Form | 解析后的 multipart 表单 |
Part | multipart 消息的单个部分 |
Reader | multipart 消息迭代器(解析) |
Writer | multipart 消息生成器 |
FileHeader 方法
| 方法 | 功能 |
|---|---|
Open() | 打开关联的文件 |
Form 方法
| 方法 | 功能 |
|---|---|
RemoveAll() | 删除所有临时文件 |
Part 方法
| 方法 | 功能 |
|---|---|
Close() | 关闭部分 |
FileName() | 获取文件名 |
FormName() | 获取字段名 |
Read(d) | 读取内容 |
Reader 方法
| 方法 | 功能 | 返回 |
|---|---|---|
NewReader(r, boundary) | 创建 Reader | *Reader |
NextPart() | 获取下一个 Part | *Part, error |
NextRawPart() | 获取原始 Part | *Part, error |
ReadForm(maxMemory) | 解析整个表单 | *Form, error |
Writer 方法
| 方法 | 功能 | 返回 |
|---|---|---|
NewWriter(w) | 创建 Writer | *Writer |
Boundary() | 获取边界 | string |
Close() | 完成消息 | error |
CreateFormField(name) | 创建字段 | io.Writer, error |
CreateFormFile(field, filename) | 创建文件字段 | io.Writer, error |
CreatePart(header) | 创建自定义部分 | io.Writer, error |
FormDataContentType() | 获取 Content-Type | string |
SetBoundary(boundary) | 设置边界 | error |
WriteField(name, value) | 写入字段 | error |
Form 结构
type Form struct {
Value map[string][]string // 普通字段
File map[string][]*FileHeader // 文件字段
}
八、注意事项
1. 内存限制
// ReadForm 的内存限制包括:
// - maxMemory: 用于存储文件的内存
// - 额外 10MB: 保留用于非文件部分
// ✓ 正确:设置合理限制
form, err := reader.ReadForm(32 << 20) // 32MB + 10MB
// 超出部分会自动存储到磁盘临时文件
2. 资源清理
// ✓ 正确:始终清理
form, err := reader.ReadForm(32 << 20)
if err != nil {
return err
}
defer form.RemoveAll() // 即使出错也要清理
// Part 也需要关闭
part, err := reader.NextPart()
if err != nil {
return err
}
defer part.Close()
3. 边界字符串
// Writer 自动生成随机边界
writer := multipart.NewWriter(&buf)
boundary := writer.Boundary() // 随机生成
// 如需自定义,必须在创建任何部分之前
writer.SetBoundary("MyBoundary") // ✓
writer.CreateFormField("f") // 创建部分后
writer.SetBoundary("MyBoundary") // ✗ 报错
// 边界限制:
// - 必须非空
// - 最多 70 字节
// - 只能包含某些 ASCII 字符
4. 写入顺序
// Writer 必须按顺序写入
writer := multipart.NewWriter(&buf)
part1, _ := writer.CreateFormField("field1")
part2, _ := writer.CreateFormField("field2")
// 创建 part2 后,不能再写入 part1
part1.Write([]byte("data")) // ✗ 可能失败或导致数据损坏
// ✓ 正确:按顺序写入
part1.Write([]byte("data"))
part2.Write([]byte("data"))
5. Close 调用
// Writer 必须调用 Close 完成消息
writer := multipart.NewWriter(&buf)
writer.WriteField("field", "value")
// 忘记调用 Close() // ✗ 消息不完整
// ✓ 正确
writer.WriteField("field", "value")
writer.Close() // 写入结束边界
6. NextPart vs NextRawPart
// NextPart: 自动处理 quoted-printable 编码
part, _ := reader.NextPart()
// Content-Transfer-Encoding: quoted-printable 会被隐藏
// Read 时自动解码
// NextRawPart: 返回原始数据
rawPart, _ := reader.NextRawPart()
// 保留 Content-Transfer-Encoding 头部
// Read 时不解码
// 选择依据:
// - 需要解码内容:使用 NextPart
// - 需要原始数据:使用 NextRawPart
7. 安全限制
// 包内置安全限制防止恶意输入:
// - 每个 part 最多 10000 个头部
// - Form 中最多 1000 个 part
// - 可调整:GODEBUG=multipartmaxheaders=20000
// GODEBUG=multipartmaxparts=2000
// 但仍需设置合理的内存限制
form, err := reader.ReadForm(32 << 20) // 防止内存耗尽
8. 文件名处理
// FileName() 会通过 filepath.Base 处理
// 这可能导致平台相关行为
part.FileName()
// Unix: "/path/to/file.txt" -> "file.txt"
// Windows: "C:\\path\\to\\file.txt" -> "file.txt"
// ✓ 正确:不要信任文件名
filename := filepath.Base(part.FileName()) // 再次确保
// ✗ 错误:直接使用
filename := part.FileName() // 可能包含路径
9. Content-Type 检测
// CreateFormFile 自动检测 Content-Type
// 基于文件扩展名
writer.CreateFormFile("file", "photo.jpg")
// 自动设置 Content-Type: image/jpeg
// 未知扩展名使用 application/octet-stream
writer.CreateFormFile("file", "unknown.xyz")
// Content-Type: application/octet-stream
// ✓ 正确:需要自定义 Content-Type 时使用 CreatePart
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", `form-data; name="file"; filename="data"`)
h.Set("Content-Type", "application/custom")
writer.CreatePart(h)
最后更新: 2026-04-05
Go 版本: Go 1.0+
包文档: https://pkg.go.dev/mime/multipart
相关 RFC: RFC 2046 (MIME), RFC 2388 (HTTP Form)