必须先判断 err == nil 再 defer resp.Body.Close(),且需显式检查 resp.StatusCode >= 400;Content-Type 和 Content-Disposition 头不可省;大文件须用 io.Copy 流式转发。

resp.Body.Close() 必须在 err == nil 后立刻 defer
很多人写 defer resp.Body.Close() 看似稳妥,但一旦 resp 是 nil(比如 http.Get 返回 err != nil),defer 会 panic。更隐蔽的问题是:即使读取失败(如网络中断、解码错误),你也得关掉它——否则连接卡在连接池里,下次复用可能读到脏数据。
正确姿势是:先判断 err == nil,再在该分支开头立刻加 defer resp.Body.Close()。哪怕你后续用 io.Copy 或 json.NewDecoder 流式处理,也必须关。
- 错误写法:
body, _ := io.ReadAll(resp.Body)后才关,中间出错就漏关 - 安全写法:只在
if err == nil分支内第一行写defer resp.Body.Close() - 流式转发也要关:哪怕用了
io.Copy(w, resp.Body),仍需手动关(io.Copy不负责关闭)
Content-Type 和 Content-Disposition 头不能省
浏览器对文件响应的处理高度依赖响应头。只写 http.ServeFile 或直接 w.Write(body),很可能触发 inline 渲染而非下载,或因编码问题损坏二进制内容。
Content-Type 设为 "application/octet-stream" 最保险;若知类型(如 PDF),可用 "application/pdf"。Content-Disposition 必须含 attachment; filename="xxx.pdf",否则多数浏览器当页面渲染。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Content-Length建议显式设置,避免 chunked 编码引发客户端解析异常(尤其某些旧版下载工具) - 不要依赖浏览器自动猜测 MIME 类型,它不可靠,尤其对 ZIP、EXE、自定义二进制格式
- filename 值建议用双引号包裹,避免空格或特殊字符被截断
大文件必须用 io.Copy,禁用 io.ReadAll
用 io.ReadAll(resp.Body) 加载整个文件到内存,100MB 文件就吃掉 100MB 堆内存,高并发下直接 OOM。Go 标准库提供了更优雅的流式方案:直接管道化 http.Response.Body 到 http.ResponseWriter。
io.Copy 内部采用固定缓冲区(默认 32KB),循环读取并写入,全程无须加载全部数据到内存,恒定内存占用(O(1))。
- 别写
w.Write(bodyBytes)—— 这等于把整块内存复制一遍,且已绕过io.Copy的缓冲优化 - 如果上游响应没设
Content-Length,io.Copy仍能正常工作,只是客户端无法显示进度条 - 遇到客户端断连时,
io.Copy可能返回io.ErrUnexpectedEOF或net/http.ErrAbortHandler,通常可忽略,但建议打日志便于排查
状态码检查不能只看 err != nil
http.Get 的 err 只管网络层(连不上、超时、TLS 失败等),不管业务逻辑。404、500、422 全都返回 err == nil,但 resp.StatusCode 已不是你想的那样了。
非 2xx 响应体往往含错误详情(如 {"error": "invalid_token"}),建议读出来并包装进自定义 error;304 响应体为空,但头里有缓存信息,别直接 io.ReadAll 报 panic。
- 必须显式检查:
if resp.StatusCode >= 400,不能只靠if err != nil - 重定向(301/302)默认自动跟随,若需拦截,得配
Client.CheckRedirect - 即使你只转发,也建议透传上游状态码:
w.WriteHeader(resp.StatusCode)
resp.Body.Close() 的执行时机——它们不出现在编译错误里,也不报 panic,只在高并发或异常响应时悄悄拖垮服务。写完请求逻辑,花三秒确认这两行有没有:一行是 if resp.StatusCode >= 400,一行是 defer resp.Body.Close()。

















