Example: io.ReadSeeker and io.WriteSeeker
Sometimes a stream needs more than sequential access.
You may need to read a header, jump to another position, then continue reading. Or write data first and later seek back to fill in a value whose final size was not known yet.
That is what io.ReadSeeker and io.WriteSeeker provide:
The important property is that Seek changes the stream's current position.
If each operation already knows its offset, io.ReaderAt or io.WriterAt is usually a better fit.
Seekmoves the current position;Atuses an explicit offset.
Quick Example
Read three bytes starting at position 5:
Output:
After the Seek, the reader's current position is 5.
The subsequent Read starts there and advances the position to 8.
io.ReadSeeker
Use io.ReadSeeker when a function needs to move around the same readable stream.
For example, a parser may need to return to the beginning and read a fixed header:
The function explicitly changes the caller's current position.
That is an important part of the API contract.
A function accepting io.ReadSeeker is not merely saying:
“I need random access.”
It is saying:
“I need to control the stream's current position.”
io.WriteSeeker
io.WriteSeeker is useful when the final value of something is not known when it is first written.
A common pattern is:
For example, write a four-byte payload length before the payload:
The key idea is not the length encoding.
It is the ability to patch previously written bytes by moving the write position.
This pattern is common with file formats and other seekable output where metadata depends on data written later.
Seek Is Stateful
Consider:
The reader now has a new current position.
A later operation using the same reader depends on that state:
starts at the position established by the previous Seek.
That makes a seeker convenient for sequential parsing with occasional jumps, but it also makes it stateful.
ReaderAt Is Different
If the operation is simply:
“Give me bytes at offset 1024.”
then ReaderAt expresses that requirement directly:
It does not first move a shared cursor to 1024.
Compare:
with:
The second form is often easier to reason about when operations are independent.
Why This Matters for Concurrent Access
A seeker carries mutable position state.
This can be problematic if multiple operations share it:
The operations are both modifying the same position.
Without synchronization, one operation can move the cursor while another operation expects it to remain somewhere else.
For independent random-access operations, prefer:
or give each operation its own seeker.
Do not assume that an implementation being backed by a file makes arbitrary concurrent Seek + Read operations safe at the interface level.
Common Mistakes
1. Assuming Read Fills the Buffer
Read may return fewer bytes than requested.
If the format requires exactly the requested number:
Do not confuse:
with:
ReadSeeker provides seeking, not exact-read semantics.
2. Using Seek When You Really Need ReadAt
If each operation has an independent offset:
is usually clearer than:
The first expresses the operation directly and does not modify the current seek position.
3. Forgetting That Seek Changes Caller State
A function like:
may leave r at a different position when it returns.
If the caller expects a particular position afterward, that contract needs to be explicit.
Sometimes the function should save and restore the original position:
Whether restoring the position is appropriate depends on the API contract. Do not silently impose cursor behavior on callers.
4. Sharing a Seeker Across Concurrent Operations
This is unsafe as a general interface-level assumption:
if both functions use Seek followed by Read.
Use ReaderAt for independent offset-based reads, or synchronize access to the seeker.
5. Assuming Every Seeker Supports Arbitrary Positions
io.Seeker defines the operation, not the storage model.
Whether negative offsets, extending writes, seeking beyond the current end, or particular Seek modes are valid depends on the concrete implementation.
Check and handle the returned error.
ReadSeeker vs. ReaderAt
The distinction is easiest to remember as cursor vs. offset:
For example:
versus:
The second is usually the better abstraction when reads are independent.
WriteSeeker vs. WriterAt
The same distinction applies to writing.
Use WriteSeeker when the workflow is naturally:
Use WriterAt when the operation already knows the destination offset:
This is particularly useful when independent writes should not depend on shared cursor state.
API Selection
Rule of Thumb
Use
ReadSeeker/WriteSeekerwhen your algorithm needs to move the current position.
Use
ReaderAt/WriterAtwhen your algorithm already knows the offset.
The mental model is simple: