io.sectionreader 适合从大文件中局部、随机、只读某一段字节而不加载全量到内存的场景;它包装已打开的 io.readerat,限定读取范围,不管理文件生命周期,需手动 close。

io.SectionReader 适合什么场景
当你需要从大文件中读取某一段字节(比如跳过头部、只读中间 1MB、或分块上传时切片),又不想把整文件加载进内存,io.SectionReader 就是标准库里最轻量的解法。它不打开文件,也不管理 *os.File 生命周期,只包装一个已打开的 io.ReaderAt,然后限定读取范围。
常见误用是拿它去“读取整个文件”——这反而多了一层封装,不如直接用 io.Copy 或 io.ReadFull;它的价值只在「局部、随机、只读」。
必须传入 io.ReaderAt,不能传 *os.File 直接用
os.File 实现了 io.ReaderAt,但 io.SectionReader 的构造函数签名是 func NewSectionReader(r io.ReaderAt, off int64, n int64) *SectionReader,所以你得显式传进去,不是自动转换。
容易踩的坑:
- 传
os.Stdin或bytes.NewReader会 panic:它们不实现io.ReaderAt(没有ReadAt方法) - 传
*os.File没问题,但别忘了先os.Open,且后续仍需自己Close -
off超出文件长度不会报错,但后续Read返回 0 和io.EOF
示例:
file, _ := os.Open("data.bin")
defer file.Close()
// 从第 1024 字节开始,读取最多 4096 字节
sr := io.NewSectionReader(file, 1024, 4096)
buf := make([]byte, 512)
n, err := sr.Read(buf) // 实际读到的可能少于 512,取决于剩余长度
Read 和 ReadAt 行为差异影响调用逻辑
io.SectionReader 同时实现了 io.Reader 和 io.ReaderAt,但二者语义不同:
-
Read(p []byte)是顺序读:每次调用从当前偏移开始读,内部维护一个相对位置(从off起算) -
ReadAt(p []byte, off int64)是绝对读:忽略内部状态,直接从原始io.ReaderAt的off + sectionOff处读(sectionOff是构造时的off)
这意味着:
- 混用
Read和ReadAt容易读错位置——比如先Read了 100 字节,再ReadAt(p, 50),实际读的是原始文件的1024+50 = 1074,而非你预期的“从这段的第 50 字节” - 如果要多次随机读同一段内的不同位置,优先用
ReadAt;如果只是流式读完这一段,用Read更自然
len 和 Cap 不等于可用长度,别靠 cap(buf) 判断读满
io.SectionReader 的长度由构造时的 n int64 决定,和底层 io.ReaderAt 实际可读长度无关。它不会主动检查 n 是否超出源长度,也不会在 Read 时自动截断到真实末尾。
所以:
-
sr.Size()返回的是你传的n,不是“这段能读多少” - 真实可读字节数 =
min(n, file_size - off),需手动校验(例如用file.Stat().Size()) - 用
io.ReadFull(sr, buf)可能返回io.ErrUnexpectedEOF,因为buf长度 > 实际剩余字节数
安全做法是用循环 Read 或配合 io.LimitReader 做二次限制。
真正要注意的是:SectionReader 不持有文件句柄,也不做 seek 或 close,所有资源管理仍在你手上。很多人以为它“封装了文件”,结果忘了 Close,导致 fd 泄漏。











