How to Add Seeking Capabilities to an io.Reader Using ioseek
The ioseek package from the aperturerobotics/util repository wraps any io.ReaderAt implementation with a ReaderAtSeeker struct that implements both io.Reader and io.Seeker interfaces, enabling random access to streams that originally only supported sequential reading.
The ioseek package in the aperturerobotics/util repository provides a lightweight solution for adding seeking capabilities to Go readers. By wrapping an io.ReaderAt implementation with the ReaderAtSeeker type defined in ioseek/reader-at-seeker.go, you can convert any data source that supports random offset reading into a full io.ReadSeeker compatible with standard library functions and third-party packages.
Understanding the ioseek Package Structure
The core functionality resides in ioseek/reader-at-seeker.go, which defines the ReaderAtSeeker struct. This type maintains three critical pieces of state: the underlying io.ReaderAt implementation, the total size of the data source, and the current seek offset. The package uses compile-time interface assertions to guarantee that ReaderAtSeeker satisfies the io.ReadSeeker interface.
How ReaderAtSeeker Works
Core Structure and Constructor
The ReaderAtSeeker struct encapsulates an io.ReaderAt and tracks the current position. You create an instance using NewReaderAtSeeker(readerAt, size), passing both the reader and the total byte size of the underlying data. The size parameter is essential because it enables bounds checking during seek operations.
Seek Implementation
The Seek(offset, whence) method implements the io.Seeker interface. It supports all three standard seek constants: io.SeekStart, io.SeekCurrent, and io.SeekEnd. The method validates that the resulting offset remains within the bounds [0, size]. Attempting to seek before the start returns an error, while seeking past the end returns io.EOF.
Read Implementation
The Read(p) method calls the underlying ReaderAt.ReadAt(p, offset) using the current internal offset, then advances the offset by the number of bytes actually read. This approach fulfills the io.Reader contract while leveraging the random access capabilities of the underlying ReaderAt.
Practical Examples
Wrapping a bytes.Reader
The most common use case involves wrapping an in-memory byte slice. Since bytes.Reader implements io.ReaderAt, you can convert it to a seekable stream:
data := []byte("Hello, world! This is a test.")
br := bytes.NewReader(data)
// Create the seekable wrapper
rs := ioseek.NewReaderAtSeeker(br, int64(len(data)))
// Seek to the 7th byte (start of "world")
pos, err := rs.Seek(7, io.SeekStart)
if err != nil {
log.Fatal(err)
}
fmt.Println("Position after Seek:", pos)
// Read 5 bytes
buf := make([]byte, 5)
n, err := rs.Read(buf)
if err != nil && err != io.EOF {
log.Fatal(err)
}
fmt.Printf("Read: %s\n", string(buf[:n])) // Output: world
Converting an os.File
For file operations, os.File already implements io.ReaderAt. You can wrap it to create a reusable io.ReadSeeker with explicit size bounds:
f, err := os.Open("example.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Get file size for bounds checking
fi, err := f.Stat()
if err != nil {
log.Fatal(err)
}
// Wrap the file
rs := ioseek.NewReaderAtSeeker(f, fi.Size())
// Read last 10 bytes
_, err = rs.Seek(-10, io.SeekEnd)
if err != nil {
log.Fatal(err)
}
tail := make([]byte, 10)
n, err := rs.Read(tail)
if err != nil && err != io.EOF {
log.Fatal(err)
}
fmt.Printf("Last 10 bytes: %s\n", string(tail[:n]))
Custom ReaderAt Implementation
For specialized data sources, implement io.ReaderAt and wrap it with ioseek:
type memReaderAt struct {
data []byte
}
func (m *memReaderAt) ReadAt(p []byte, off int64) (int, error) {
if off >= int64(len(m.data)) {
return 0, io.EOF
}
n := copy(p, m.data[off:])
if n < len(p) {
return n, io.EOF
}
return n, nil
}
// Usage
m := &memReaderAt{data: []byte("custom data source")}
rs := ioseek.NewReaderAtSeeker(m, int64(len(m.data)))
// Use as io.ReadSeeker
buf := make([]byte, 6)
rs.Read(buf)
fmt.Println(string(buf)) // Output: custom
Summary
- The
ioseekpackage inaperturerobotics/utilprovides theReaderAtSeekertype to convert anyio.ReaderAtinto a fullio.ReadSeeker. - The implementation resides in
ioseek/reader-at-seeker.goand uses compile-time interface checks to ensure compatibility. - You must provide the total data size when constructing the wrapper to enable proper bounds checking during seek operations.
- The wrapper supports all standard seek origins (
SeekStart,SeekCurrent,SeekEnd) and handles edge cases like seeking past the end of data. - This approach works with
bytes.Reader,os.File, or any customio.ReaderAtimplementation.
Frequently Asked Questions
What is the difference between io.Reader and io.ReadSeeker?
io.Reader provides only sequential access through the Read method, while io.ReadSeeker extends this with the Seek method to enable random access at arbitrary offsets. The ioseek package bridges this gap by wrapping io.ReaderAt implementations to provide seeking capabilities without requiring the underlying source to implement the full io.ReadSeeker interface natively.
Why do I need to provide the size when creating a ReaderAtSeeker?
The NewReaderAtSeeker constructor requires the total size of the underlying data to enforce bounds checking during seek operations. According to the implementation in ioseek/reader-at-seeker.go, this size validates that seek offsets remain within the valid range [0, size], preventing seeks before the start of the data and properly returning io.EOF when seeking past the end.
Can I use ioseek with network streams?
Network streams typically implement io.Reader but not io.ReaderAt because they do not support random access. To use ioseek, the underlying source must implement io.ReaderAt, which requires the ability to read from arbitrary offsets. For network resources, you would need to buffer the entire stream into memory or use a temporary file to obtain an io.ReaderAt implementation before wrapping it with ioseek.NewReaderAtSeeker.
Where can I find the source code for ioseek?
The ioseek package is part of the aperturerobotics/util repository on GitHub. The core implementation resides in ioseek/reader-at-seeker.go, which defines the ReaderAtSeeker struct and its methods. You can view the complete source code at the repository URL: https://github.com/aperturerobotics/util.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →