Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go mime/multipart 包详解

概述

mime/multipart 包实现了 MIME multipart(多部分)消息的解析和生成,定义于 RFC 2046。该实现足够处理 HTTP(RFC 2388)以及流行浏览器生成的 multipart 消息体。包中提供了 ReaderWriter 两种主要类型,分别用于解析和生成 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"])

七、快速参考

变量

变量类型说明
ErrMessageTooLargeerror消息太大无法处理

函数

函数功能返回值
FileContentDisposition(fieldname, filename)生成 Content-Disposition 头部string

类型

类型功能
File文件接口(io.Reader/ReaderAt/Seeker/Closer)
FileHeader文件部分头部描述
Form解析后的 multipart 表单
Partmultipart 消息的单个部分
Readermultipart 消息迭代器(解析)
Writermultipart 消息生成器

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-Typestring
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)