Go HTTP 文件上传需先调用 ParseMultipartForm 设置内存阈值(如32
request.ParseMultipartForm 读取 multipart/form-data
Go 标准库不自动解析 multipart 表单,必须显式调用 ParseMultipartForm,否则 request.FormFile 会返回 nil, http.ErrNotMultipart。
FormFile 前设置内存阈值:err := r.ParseMultipartForm(32 << 20) // 32MB 内存上限,超限写入临时文件
r.FormFile("file") 返回 *multipart.FileHeader,它不是文件内容,只是元信息(Filename、Size、Header)header.Open() 得到 io.ReadCloser,且必须手动 Close()
header.Size 和 header.Filename(防空名、路径遍历如 ../../etc/passwd)filepath.Join + os.Create
用户传的 Filename 可能含恶意路径,os.Create("upload/" + header.Filename) 会导致任意目录写入。
filename := filepath.Base(filepath.Clean(header.Filename))
dstPath := filepath.Join("uploads", filename)
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { /* handle */ }os.Create 而非 ioutil.WriteFile(后者无法流式处理大文件),配合 io.Copy:dst, err := os.Create(dstPath)
if err != nil { /* handle */ }
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil { /* handle */ }http.ServeFile 或手动设置 Header + io.Copy
http.ServeFile 简单但暴露绝对路径,且无法做权限校验;手动方式更可控。
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))os.Open 打开文件后,必须检查 fileInfo.Size() 是否与实际一致(防止竞态或截断)io.Copy 流式传输,避免全量加载到内存:file, _ := os.Open(filePath) defer file.Close() io.Copy(w, file)
filepath.Clean 并校验是否在白名单目录内(如 strings.HasPrefix(absPath, allowedRoot))Go 的 ParseMultipartForm 默认将大文件写入 /tmp,但不会自动清理——上传失败或 panic 后临时文件滞留。
立即学习“go语言免费学习笔记(深入)”;
Content-Type(可伪造),用 filetype 库或 net/http.DetectContentType 检查前 512 字节uuid.NewString() + 原始名)defer os.Remove(header.Filename) 不可靠(路径不对);正确做法是用 header.Open() 后拿到的 *os.File 对象,在读完后调用 file.Close(),系统会自动清理其关联的临时文件net/http 不支持;需改用 gobuffalo/packr/v2 或自定义中间件包装 http.Request.Body
文件上传下载本身不难,难的是边界校验和资源生命周期管理——尤其是临时文件何时删、锁粒度怎么控、错误路径下 Close 是否被遗漏。这些地方一
漏,服务跑几天就磁盘告警。