




必须提前调用 ParseMultipartForm,否则 r.MultipartForm 和 r.FormFile 返回空值或 nil;它解析表单边界并限制内存缓冲(默认32MB,超限写入临时磁盘文件)。
http.Request.ParseMultipartForm 必须提前调用不调用 ParseMultipartForm 就直接访问 r.MultipartForm 或 r.FormFile,会得到空值或 nil,且不会报错——这是最常被忽略的前置步骤。
它实际做了两件事:解析表单边界、限制内存缓冲大小。默认只读取 32MB 内存,超出部分写入临时磁盘文件(由 os.TempDir() 决定)。
r.FormValue 和 r.FormFile 都不可靠r.ParseMultipartForm(32 表示 32MB 内存上限
math.MaxInt64,但不推荐——可能 OOMr.MultipartForm.File 才包含上传的文件元信息r.FormFile 返回的是 *multipart.FileHeader,不是文件内容r.FormFile("avatar") 只返回一个描述文件的结构体,含 Filename、Size、Header 等字段,真正内容要靠 Open() 打开流读取。
常见错误是直接打印 fileHeader 认为拿到了数据,或者忘记 Close() 导致句柄泄漏。
file, handler, err := r.FormFile("file") 中的 file 是 multipart.File 类型(实现了 io.ReadCloser)file.Close(),尤其在循环处理多个文件时handler.Size 是客户端声明的大小,不可信;应边读边校验实际读取字节数io.Copy 而非一次性 io.ReadAll,避免大文件撑爆内存../ 防止目录遍历用户提交的 Filename 是完全不可信的。若直接拼进 os.OpenFile 路径,比如 "uploads/" + header.Filename,攻击者传 ../../etc/passwd 就能写入任意位置。
标准做法是丢弃原始文件名,用服务端生成的唯一 ID 命名,并严格限定保存根目录。
path.Base(header.Filename) 提取基础名,再用 strings.TrimSuffix 去掉可疑后缀(如 .php)filepath.Join(uploadDir, safeName) 拼路径,之后用 filepath.Rel(uploadDir, fullPath) 反向验证是否仍在目录内header.Filename,用 uuid.New().String() + filepath.Ext(header.Filename)
uploadDir 是否为绝对路径,且 os.Stat(uploadDir).IsDir() 为 trueGo 默认 HTTP server 没有请求体大小硬限制,但生产环境几乎总是前置了 Nginx。如果 Nginx 的 client_max_body_size 设为 10M,而 Go 层还傻等 100M 数据,会导致连接卡住、超时、502 错误。
Go 自身也要设超时,否则慢速上传(如网络抖动)可能长期占用 goroutine。
client_max_body_size 50M;,并确认 client_body_timeout 足够长ReadTimeout 和 WriteTimeout(例如 30 秒),避免慢连接堆积multipart,改用自定义协议 + io.Pipe 流式接收multipart 获取,需前端用 XMLHttpRequest.upload.onprogress 或后端引入中间层(如 tusd)文件上传看着简单,真正上线时出问题的点往往不在 Go 代码本身,而在边界

ParseMultipartForm 的调用时机和 file.Close() 的遗漏,线上查起来特别隐蔽。