必须在读取前判 err == nil 后立即 defer resp.Body.Close(),否则 resp 为 nil 或读取失败时连接泄漏;大文件须用 io.Copy 流式处理,禁用 ioutil.ReadAll 防 OOM。

resp.Body.Close() 必须在读取完成后立刻调用,不能靠 defer 撑到函数末尾
很多人写 defer resp.Body.Close() 看似稳妥,但一旦 resp 是 nil(比如 http.Get 返回 err 不为 nil),defer 会 panic。更隐蔽的问题是:如果读取 body 失败(如网络中断、解码错误),你仍需关掉它——否则连接卡在连接池里,下次复用可能读到脏数据。
正确姿势是:先判 err == nil,再立刻 defer resp.Body.Close(),且必须在读取逻辑之前。
- 错误写法:
body, _ := io.ReadAll(resp.Body)后才关,中间出错就漏关 - 安全写法:在
if err == nil分支开头加defer resp.Body.Close() - 流式读取(如大文件下载)也要关:哪怕用了
io.Copy(dst, resp.Body),也得手动关
Content-Type 和 Content-Disposition 头必须显式设置,不能依赖浏览器猜测
浏览器对文件响应的处理高度依赖响应头。只写 http.ServeFile 或直接 w.Write(body),很可能触发 inline 渲染而非下载,或因编码问题损坏二进制内容。
关键头有三个:
-
Content-Type:设为"application/octet-stream"最保险;若知类型(如 PDF),可用"application/pdf" -
Content-Disposition:必须含attachment; filename="xxx.pdf",否则多数浏览器当页面渲染 -
Content-Length:建议显式设置,避免 chunked 编码引发客户端解析异常(尤其某些旧版下载工具)
示例:
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", "attachment; filename=\"report.pdf\"")
w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
io.Copy(w, file)
大文件响应要用 io.Copy,别用 io.ReadAll + Write
用 io.ReadAll(resp.Body) 加载整个文件到内存,100MB 文件就吃掉 100MB 堆内存,高并发下直接 OOM。Go 的 http.ResponseWriter 本身支持流式写入,io.Copy 就是为此设计的。
- 小文件(io.ReadAll +
w.Write,但要加io.LimitReader防恶意超大响应 - 中大文件(≥5MB):必须用
io.Copy(w, reader),底层走 buffer 复制,内存占用恒定 ~32KB - 注意:若需修改响应体(如加水印、加密),得自己实现
io.Reader包装器,不能绕过流式路径
文件名含中文或特殊字符时,filename* 参数比 filename 更可靠
Content-Disposition: attachment; filename="简历.pdf" 在 Chrome/Firefox 可能乱码,因为 RFC 5987 要求非 ASCII 字符必须用 filename* 编码。单纯设 filename 是兼容性妥协,不是正确解法。
- 正确做法:同时设置两个字段
w.Header().Set("Content-Disposition", `attachment; filename="resume.pdf"; filename*=UTF-8''%E7%AE%80%E5%8E%86.pdf`) - 编码工具可用
url.PathEscape处理 UTF-8 字节序列,再拼进filename* - 不推荐用
mime.BEncoding.Encode——它针对邮件场景,HTTP 头里反而易出错
最常被忽略的是顺序:Header 设置必须在第一次 w.Write 或 io.Copy 之前完成;而 Content-Length 若无法预知(如动态生成的 zip),宁可不用,也不要设错——浏览器会等超时或直接断连。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











