ctx.Attachment()仅支持本地文件路径,不支持内存流和大文件;动态生成文件须用ctx.SendStream()并手动设头;超大文件需启用SendFile零拷贝或手动实现Range断点续传。

ctx.Attachment() 适合小文件,但不支持内存流和大文件
直接用 ctx.Attachment() 下载文件最省事,但它只接受本地文件路径(string),不接收 []byte 或 io.Reader。传入相对路径容易因工作目录不确定而报 open xxx: no such file or directory;传中文名默认走 RFC 5987 编码,旧版 IE 可能乱码;最关键的是——它底层调用 http.ServeFile,无断点续传、不启用 SendFile 零拷贝,>100MB 就可能卡顿或 OOM。
常见误用:
- 把 bytes.Buffer 内容先写成临时文件再调 Attachment() → 多一次磁盘 IO,没必要
- 在 handler 里先调 c.FormFile() 再设 MaxBytesReader → 请求体已开始读,限制失效
- 用 ctx.Status(200).JSON() 混合返回 → HTTP 响应体类型冲突,浏览器收不到文件
内存生成文件必须用 ctx.SendStream() + 手动设头
Excel、PDF、ZIP 等动态生成的文件,不能塞给 Attachment(),得靠 ctx.SendStream() 推流,但必须严格按顺序操作:
-
ctx.Set("Content-Type", "...")必须在SendStream()前调用,否则响应头丢失 -
ctx.Set("Content-Disposition", `attachment; filename="xxx.xlsx"`)手动设,避免中文名兼容问题 - 传入的
io.Reader要稳定输出完整数据,提前 EOF 会导致浏览器提示“文件损坏” - 别用
bytes.Reader包整个大 buffer —— 它把全部内容加载进内存,失去流式意义
示例(excelize 生成后下载):
buf := &bytes.Buffer{}
f := excelize.NewFile()
// ... 写入数据
f.Write(buf)
ctx.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
ctx.Set("Content-Disposition", `attachment; filename="report.xlsx"`)
ctx.SendStream(buf)超大文件(>500MB)要启用 SendFile 零拷贝并支持 Range
Fiber 默认不开启 SendFile,需显式配置且依赖 OS 支持(Linux sendfile、macOS sendfile、Windows TransmitFile)。不启用时,大文件会走常规 io.Copy,CPU 和内存压力陡增。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
启用方式很简单,但有两个关键点:
- 必须在
fiber.Config中设ServerHeader: false(可选)+ 启用DisableStartupMessage: true(非必须),真正起作用的是底层http.Server的Handler行为 - 更实际的做法:用
http.ServeFile或http.FileServer配合http.StripPrefix暴露静态路径,由标准库自动触发零拷贝 - 若需支持断点续传(
Range请求),就不能用SendStream(),得自己解析Rangeheader、用io.ReadAt定位读取、手动设206 Partial Content和Content-Range
手动实现 Range 的核心片段:
f, _ := os.Open("/path/to/big.zip")
defer f.Close()
stat, _ := f.Stat()
size := stat.Size()
rangeHeader := c.Get("Range")
// 解析 rangeHeader 得到 start, end → 用 f.ReadAt(buf, start) 读指定段分片下载本质是客户端控制 Range,服务端只做字节切片
所谓“分片下载”,不是服务端主动切文件,而是客户端发多个带 Range: bytes=0-1048575 的请求,并发拉取不同区间。服务端只需:
- 校验
Range是否越界(start ) - 用
*os.File.ReadAt()安全读取对应 offset 的 chunk,不 seek 全局文件指针 - 返回
206+ 正确的Content-Range和Content-Length - 注意:不要用
bufio.Reader包装文件再ReadAt—— 它不支持随机读,会破坏 offset
并发读安全的关键:每个 ReadAt 调用是独立的,无需锁;但若复用同一 *os.File 并混用 Seek(),就会相互覆盖偏移量。所以要么只用 ReadAt,要么为每次读新开文件句柄。

















