必须显式设置content-disposition为attachment,否则浏览器默认inline渲染;中文名需按rfc 5987编码filename*字段,大文件须流式传输避免oom,路径须严格校验防遍历。

Content-Disposition 头没设对,浏览器就当页面渲染
文件流写进 w 之后,如果没设 Content-Disposition,浏览器大概率会尝试 inline 渲染(比如 PDF 打开、JSON 显示成文本),而不是弹下载框。这不是 bug,是 HTTP 协议默认行为。
必须显式设成 attachment 模式:
w.Header().Set("Content-Disposition", `attachment; filename="report.pdf"`)- 中文名要走 RFC 5987 编码:
attachment; filename="report.pdf"; filename*=UTF-8''%E6%8A%A5%E5%91%8A.pdf - 别用 URL 编码或
url.PathEscape—— 那是给路径用的,不是给filename*用的
大文件千万别用 io.ReadAll
io.ReadAll(resp.Body) 或 os.ReadFile 把整个文件加载进内存,100MB 文件就吃掉 100MB 堆,高并发下直接 OOM。这不是理论风险,是线上真实踩过的坑。
正确做法永远是流式转发:
- 用
os.Open获取*os.File - 立刻
defer f.Close()(注意:必须在err == nil分支里 defer,否则f是 nil 会 panic) - 用
io.Copy(w, f)直接推流,底层自动分块,内存占用恒定
路径遍历漏洞比你想象中更容易触发
表单传来的 filename 或 URL 参数(如 /download?name=../etc/passwd)若直接拼进 os.Open,就是典型路径遍历。Go 不会自动过滤,filepath.Clean 也不是万能解药。
安全做法只有两种:
-
白名单映射:前端传 ID(如
id=q3_report),后端查 map 或 DB 得到绝对路径,不碰用户输入的文件名 -
强校验 + 范围限制:先
filepath.Clean,再用strings.HasPrefix(cleaned, allowedBaseDir)确保路径没逃出指定目录 - 禁止任何含
..、/、空字节的输入,扩展名也得白名单(只允许.pdf、.csv等)
为什么推荐 http.ServeContent 而不是裸写 io.Copy
http.ServeContent 不是“多此一举”,它内置了 Range(断点续传)、If-Modified-Since、ETag 和缓存控制逻辑。你自己手写几乎不可能覆盖全这些边界。
但要注意它的参数约束:
- 第三个参数是建议的文件名(用于
Content-Dispositionfallback),不是路径 - 第四个参数是
modtime,必须传fi.ModTime(),传零值会导致缓存失效 - 第五个参数是
io.ReadSeeker,*os.File满足,但bytes.Reader也行(适合内存生成的小文件) -
contentLength不能传 -1;哪怕你 Stat 出错,也得 fallback 到 0 或估算值,否则会 panic
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











