Example: io.SectionReader
io.SectionReader provides a bounded, seekable view over an io.ReaderAt.
Constructed with io.NewSectionReader(r, off, n), it exposes a logical byte range of length n starting at absolute offset off in the underlying source.
The section's read boundary is fixed: callers can seek within or beyond the logical section, but Read and ReadAt never return bytes outside [off, off+n).
Plaintext
Key Characteristics
io.SectionReader does not:
- Allocate a separate copy of the section data
- Buffer input bytes
- Mutate the underlying source's shared read offset
- Own, close, or extend the lifetime of the underlying
io.ReaderAt
It maintains its own sequential cursor while using explicit ReadAt calls against the underlying source.
Common Use Cases
-
Concurrent chunk processing: Give each worker its own
SectionReaderso workers can process different regions of the same file without synchronizing a shared file offset. -
Parsing binary container formats: Expose headers or embedded payloads such as ZIP, ELF, or MP4 regions as independent seekable readers.
-
Bounded random access: Restrict reads to a known byte range while retaining
ReadAtandSeekcapabilities.
Core Semantics
io.SectionReaderseparates two concepts:
- A fixed section boundary defined by
baseandsize.- A mutable sequential cursor used by
ReadandSeek.
ReadAtdoes not use or modify that sequential cursor. It performs a relative random-access read within the section and translates the relative offset into an absolute offset for the underlyingReaderAt.A single
*io.SectionReaderhas mutable state and should not be concurrently used by multiple goroutines. Multiple independentSectionReaderinstances can safely operate on the sameReaderAtwhen thatReaderAtsupports concurrentReadAtcalls, as*os.Filedoes.
Quick Start & Relative Offset Semantics
io.SectionReader presents positions relative to the section.
Readstarts at relative offset0and advances the internal cursor.ReadAtaccepts a relative offset and does not modify the sequential cursor.Seekchanges the sequential cursor using the section's logical coordinate space.
Go
Output:
The important point is that ReadAt and Read maintain separate notions of position:
Relative ReadAt Translation
The caller works entirely in section-relative offsets. SectionReader performs the translation to the underlying source's absolute address space.
Recovering the Underlying Source with Outer
Outer was added to io.SectionReader in Go 1.22.
It returns the original io.ReaderAt, the section's starting offset, and the original section length supplied to NewSectionReader.
Go
Output:
Outer exposes the construction parameters. It does not expose or modify the current sequential cursor.
This is useful when a higher-level component needs to inspect or reconstruct the original section relationship without relying on implementation-specific fields.
Concurrent Chunk Processing
A shared *os.File has a mutable sequential file offset. Concurrent Read calls on that shared stream therefore require coordination when workers depend on independent positions.
io.SectionReader avoids that shared-offset problem by using ReaderAt.ReadAt, which takes an explicit offset.
The important concurrency pattern is:
Each worker should have its own SectionReader.
Go
Output order is intentionally nondeterministic because the workers run concurrently:
The important guarantee is not output ordering. It is that each worker reads only its assigned range and does not modify the file's shared sequential offset.
Concurrency boundary
io.SectionReaderdoes not make an arbitraryio.ReaderAtconcurrently safe.The concurrency property comes from the combination of:
- independent
SectionReaderstate, and- a
ReaderAtimplementation that supports concurrentReadAtcalls.
*os.Filesupports concurrent method calls, includingReadAt, so it is a natural production use case.
Seek Boundary & Read Behavior
Seek changes the sequential cursor within the section's logical coordinate space.
The section's read boundary does not move when the cursor moves.
A cursor may be positioned beyond the section size:
Go
Output:
The cursor is now at logical position 100, but the section still contains only 5 readable bytes.
A subsequent Read therefore returns:
Output:
Seeking does not expand the section.
Negative Seek Positions
A seek that would produce a negative logical position fails.
For example:
The resulting position is invalid because a SectionReader cannot expose a negative logical position.
ReadAt Boundary Truncation
ReadAt uses a relative offset and does not modify the sequential cursor.
If the requested range extends beyond the section, SectionReader limits the underlying read to the section boundary.
For example:
The EOF indicates that the requested logical range extended beyond the section, even if the underlying ReaderAt itself had more data available.
io.SectionReader vs io.LimitReader
Both can restrict the amount of data exposed to a caller, but they solve different problems.
A useful rule:
Use
io.LimitReaderwhen you already have a sequential stream and only need to impose a maximum length.Use
io.SectionReaderwhen you have random-access data and need a bounded, seekable view of a specific byte range.
Conceptual Implementation
The following is a simplified conceptual model. It focuses on the relationship between the section-relative cursor and the underlying absolute offset; implementation details such as overflow handling and optimized WriteTo paths are omitted.
Go
The important transformation is:
For example:
This is the fundamental mechanism that allows the wrapper to expose an isolated logical address space over an existing random-access source.
WriteTo and io.Copy
Modern Go versions also provide an optimized WriteTo path for SectionReader.
This matters because io.Copy checks for specialized interfaces before falling back to a generic read/write loop.
Therefore, code such as:
can use the SectionReader's specialized write path rather than necessarily reducing the operation to repeated generic Read calls.
The important engineering point is:
io.SectionReaderis not merely a boundary-checking wrapper. It participates in Go's standard I/O fast-path interfaces.
When performance matters, preserving the wrapper rather than immediately converting the section into an in-memory []byte can retain streaming behavior and avoid an unnecessary full copy.
Critical Production Pitfalls
1. Sharing One SectionReader Instance
A SectionReader contains mutable sequential state.
Do not use the same instance concurrently:
Instead, create independent views:
Each wrapper owns its own cursor while both can use the same ReaderAt.
2. Confusing ReadAt with Read
ReadAt does not advance the sequential cursor.
This distinction matters when code mixes random-access inspection with sequential parsing:
A ReadAt call cannot be used as a substitute for Read when the caller expects the stream position to advance.
3. Underlying Resource Lifetime
SectionReader does not own the underlying resource.
For a file:
Closing the file invalidates subsequent operations through the SectionReader.
The lifetime relationship is therefore:
Creating a SectionReader does not acquire ownership of that resource.
4. Section Bounds Are Not Source Bounds
A section limits what the SectionReader can expose. It does not change the underlying source.
For example:
means:
The original file remains fully accessible through file or another ReaderAt.
This makes SectionReader a view, not a capability boundary or security sandbox.
Decision Matrix
Use the following matrix to evaluate whether io.SectionReader is the correct tool: