# How to Add Seeking Capabilities to an io.Reader Using ioseek

> Add seeking capabilities to any io.Reader using ioseek. This Go package from aperturerobotics/util wraps your reader, enabling random access for efficient data retrieval.

- Repository: [Aperture Robotics/util](https://github.com/aperturerobotics/util)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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:

```go
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:

```go
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`:

```go
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 `ioseek` package in `aperturerobotics/util` provides the `ReaderAtSeeker` type to convert any `io.ReaderAt` into a full `io.ReadSeeker`.
- The implementation resides in [`ioseek/reader-at-seeker.go`](https://github.com/aperturerobotics/util/blob/main/ioseek/reader-at-seeker.go) and 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 custom `io.ReaderAt` implementation.

## 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`](https://github.com/aperturerobotics/util/blob/main/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`](https://github.com/aperturerobotics/util/blob/main/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.