登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go multipart.Part.FileName 怎样读取上传文件名

来源:17golang原创

时间:2026-11-01 23:29:30 434浏览 收藏

处理 Go 的文件上传时,客户端传来的文件名不在请求体里单独出现,而是作为 multipart 部件的 Content-Disposition 参数保存。拿到 *multipart.Part 后,直接调用 part.FileName() 就能读取这个 filename 参数;如果参数为空或头部无法解析,返回值也可能为空。

官方文档:https://pkg.go.dev/mime/multipart#Part.FileName

但这个返回值只适合当作“不可信的原始名称”使用。Go 标准库会在非空时通过平台相关的 filepath.Base 去掉目录信息,应用仍然应该自行限制长度、生成服务端存储名,并把文件名和文件内容分开处理。

Go multipart.Part.FileName 与 Content-Disposition filename 参数关系说明图
图1:结构说明图,展示 FileName 读取 filename 参数,并把结果交给 Part 调用方。

FileName 读取的到底是什么

multipart.Part 代表 multipart body 中的一个部件。它的 Header 保存该部件的 MIME 头,FileName 读取的是 Content-Disposition 中名为 filename 的参数,而不是 FormName 返回的表单字段名,也不是文件内容。

package main

import (
	"fmt"
	"mime/multipart"
)

func showName(part *multipart.Part) {
	// FileName 只读取 Content-Disposition 的 filename 参数。
	name := part.FileName()
	if name == "" {
		// 没有文件名参数,或部件头无法按 MIME 参数解析时,按无名部件处理。
		fmt.Println("没有可用的上传文件名")
		return
	}

	// 这里的 name 仍然来自客户端,只能作为展示或后续命名策略的输入。
	fmt.Println("客户端文件名:", name)
}

例如下面的部件头把 report.csv 作为文件名参数:

Content-Disposition: form-data; name="upload"; filename="report.csv"
Content-Type: text/csv

此时 FileName() 返回 report.csv,而 FormName() 返回 upload。两者用途不同:前者描述客户端给出的文件名,后者描述表单字段。

逐个部件读取文件名和内容

如果你使用底层的 multipart.Reader,通常用 NextPart 逐个取得部件。读取下一个部件前,标准库会处理当前部件尚未消费的内容;应用要在拿到部件后及时读取或关闭它,并对解析错误和内容复制错误分别处理。

package main

import (
	"fmt"
	"io"
	"mime/multipart"
)

func readParts(reader *multipart.Reader) error {
	for {
		part, err := reader.NextPart()
		if err == io.EOF {
			// 没有更多部件,正常结束解析。
			return nil
		}
		if err != nil {
			// 头部或 multipart 边界解析失败,不能继续猜文件名。
			return fmt.Errorf("读取 multipart 部件失败: %w", err)
		}

		name := part.FileName()
		if name == "" {
			// 非文件字段没有 filename,读取或关闭后再处理下一个部件。
			if _, err := io.Copy(io.Discard, part); err != nil {
				return fmt.Errorf("消费普通字段失败: %w", err)
			}
			continue
		}

		// 示例只展示读取边界;生产代码应把内容写入受控的服务端目标。
		if _, err := io.Copy(io.Discard, part); err != nil {
			return fmt.Errorf("读取文件 %q 内容失败: %w", name, err)
		}
		fmt.Println("收到文件:", name)
	}
}

不要把“能拿到文件名”理解为“上传一定成功”。FileName 只处理部件头中的元数据,文件内容是否完整、大小是否允许、存储是否成功,仍由后续读取和保存逻辑负责。

filepath.Base 会替你做什么

官方实现对非空的 filename 调用 filepath.Base。这意味着类似 images/report.csv 的目录部分不会原样交给调用方,返回值更接近一个基础文件名。这个行为符合 multipart 上传中不应使用客户端目录信息的安全边界。

同时,filepath.Base 是平台相关的路径处理函数。不要把它当作完整的跨平台文件名清洗器,也不要把清洗后的结果直接拼接到任意目录。尤其要注意:

  • FileName() 不会替你检查文件扩展名是否符合业务要求;
  • 它不限制名称长度,也不替你处理同名覆盖、保留名或存储配额;
  • 它只处理头部参数,文件内容仍可能包含与你预期不同的数据;
  • 头部解析失败时,标准库不会凭空猜一个文件名,应用应按无名部件或错误处理。

落盘前怎样划分文件名权限

生产上传接口可以保留客户端名称用于页面展示或审计,但存储路径应由服务端生成。一个常见边界是:文件内容写入随机或业务生成的对象名,原始文件名存入元数据,响应给用户时再使用经过长度和字符策略处理的展示名。

package main

import (
	"fmt"
	"mime/multipart"
	"path/filepath"
)

type UploadMeta struct {
	OriginalName string
	StorageName  string
}

func buildMeta(part *multipart.Part, storageName string) (UploadMeta, error) {
	name := part.FileName()
	if name == "" {
		return UploadMeta{}, fmt.Errorf("上传部件缺少文件名")
	}

	// 再取一次基础名只用于明确展示边界,不把客户端路径当作存储路径。
	displayName := filepath.Base(name)
	if displayName == "." || displayName == string(filepath.Separator) {
		return UploadMeta{}, fmt.Errorf("文件名无效")
	}

	// storageName 应由服务端生成,不能由 displayName 直接拼接目录得到。
	return UploadMeta{OriginalName: displayName, StorageName: storageName}, nil
}
Go 上传文件名从客户端输入到服务端存储与审计的安全边界说明图
图2:边界说明图,展示客户端名称只能作为元数据,服务端存储名和路径必须由应用控制。

如果接口还需要限制大小,应在读取内容时配合请求大小限制或受控 reader;如果需要限制类型,应根据业务允许列表和实际内容判断,而不是只相信文件名后缀。文件名、内容、存储名和审计记录是四个不同字段,分开建模比把它们揉成一个路径更容易维护。

常见误区与发布前检查

现象原因处理方式
把字段名当文件名FormName 与 FileName 语义不同分别保存表单字段和客户端名称
保存路径出现客户端目录直接使用了未经控制的输入只保留展示名,服务端生成存储名
没有文件名却继续保存非文件字段或头部解析失败按空值分支处理,并消费或关闭部件
只校验扩展名扩展名不是内容类型证明结合大小、允许类型和实际内容策略判断

最后可以把检查收敛成四个问题:是否正确区分了 FormName 和 FileName;是否处理了空文件名;是否让服务端掌控路径和存储名;是否在读取失败、大小超限和同名场景下保留明确错误。

结论与常见追问

multipart.Part.FileName() 读取的是 Content-Disposition 的 filename 参数,非空时会通过 filepath.Base 去掉目录信息。它解决的是“从部件头拿到客户端文件名”,不解决上传内容校验、大小限制、文件类型识别或服务端存储命名。把文件名当作不可信元数据,把内容和存储路径交给服务端控制,才是更稳妥的生产边界。

为什么 FileName 返回空字符串?

部件可能只是普通表单字段,没有 filename 参数;也可能是 Content-Disposition 无法按 MIME 参数解析。应用应先判断空值,再决定跳过、记录或返回错误。

FileName 返回的名称可以直接拼接到上传目录吗?

不建议。即使标准库剥离了目录信息,客户端名称仍不应决定服务端路径。应生成受控存储名,并把原始名称单独作为元数据保存。

NextPart 和 FileName 是什么关系?

NextPart 负责取得下一个 multipart 部件,FileName 负责从该部件的 Content-Disposition 参数读取文件名;两者分别属于部件迭代和元数据访问。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>