正确设置Content-Disposition头可触发浏览器下载对话框,需用mime.FormatMediaType设置filename*(UTF-8编码+url.PathEscape)并保留ASCII的filename fallback,且必须在ServeFile或io.Copy前设置Header。

Go HTTP服务中设置Content-Disposition头触发浏览器下载
浏览器是否弹出保存对话框,取决于 Content-Disposition 响应头是否正确设置为 attachment,且文件名编码符合 RFC 5987(现代浏览器)或 RFC 2231(旧版兼容)。只写 Content-Disposition: attachment; filename="中文.pdf" 在 Chrome/Firefox 里大概率乱码或截断。
实操建议:
- 用
mime.FormatMediaType拼接带 UTF-8 编码的filename*字段,同时保留 ASCII 的filename作为降级 fallback - 文件名中的空格、括号、中文等必须用
url.PathEscape编码,不能直接用url.QueryEscape(它会把斜杠也转义,而filename*不允许斜杠) - 务必在调用
http.ServeFile或io.Copy前设置 Header,否则会 panic:「http: superfluous response.WriteHeader」
示例关键片段:
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{
"filename": "report.pdf",
"filename*": "UTF-8''" + url.PathEscape("财务报表_2024.pdf"),
}))
http.ServeFile(w, r, "/path/to/file.pdf")
为什么用http.ServeFile不如手动io.Copy更可控
http.ServeFile 内部会自动设置 Content-Disposition 为 inline,并忽略你之前设的 header;它还强制添加 Last-Modified 和 ETag,对动态生成或权限校验后的文件不适用。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 对需鉴权、限速、分片或流式生成的文件,改用
io.Copy+os.Open(或bytes.NewReader) - 手动设置
Content-Length(可选但推荐),避免 Transfer-Encoding: chunked 导致某些客户端无法显示进度条 - 如果文件很大,用
http.ServeContent支持断点续传,但它要求你提供modtime和size,且仍需自己设Content-Disposition
Go标准库对中文文件名的实际兼容表现
Chrome 90+、Firefox 80+、Edge 90+ 都能正确解析 filename*=UTF-8''%E8%B4%A2%E5%8A%A1.pdf;Safari 15.4+ 开始支持,但 Safari 14 及更早版本只认 filename 的 ASCII 子集(下划线、短横、数字、字母),遇到中文直接 fallback 到 download.bin。
实操建议:
- 永远同时提供
filename和filename*,不要省略前者 - 避免在
filename中使用中文、空格、引号、分号——哪怕只是做 fallback - 测试时别只看 Chrome,用 Safari 抓包确认响应头是否含
filename=,且值是纯 ASCII
常见错误现象与定位方法
用户点了下载链接却打开 PDF 而非保存,或文件名变成 download.bin / undefined.pdf,基本是 header 设置时机或格式错了。
排查步骤:
- 用
curl -I http://localhost:8080/download看响应头是否含Content-Disposition: attachment;,且无拼写错误(比如写成content-disposition小写首字母,虽然 HTTP 不区分大小写,但 Go 的Header.Set是精确匹配 key 的) - 检查是否在
http.ServeFile后又调用了w.Header().Set—— 这会触发 panic 并返回 500,但前端可能只看到空白页 - 用浏览器 DevTools → Network → Headers 查看原始响应头,确认
filename*的值是否被双引号包裹、UTF-8''前缀是否完整、百分号编码是否合法
最易被忽略的是:设置了 header 却忘了 http.ServeFile 会覆盖它,或者用 url.QueryEscape 编码了路径分隔符,导致 filename* 值非法,浏览器静默忽略整个字段。










