Why can't you rewind an io.Reader, and how do you tell if a stream supports it?
answer
- one method, no position
- capability, not a kind of thing
- some readers implement a second interface
- the comma-ok probe
- better to demand it in the signature
basics
~20 sio.Reader declares only Read. It has no position and no memory of what it handed out, so there is nothing to go back to. Only implementations that also satisfy io.Seeker or io.ReaderAt can re-read, which you check with a type assertion.
solid answer
~50 s`io.Reader` is a single-method contract: `Read(p []byte) (n int, err error)`. It has no offset, no history and no way to un-consume bytes, so "rewind" is not part of what a reader promises. Random access is a separate capability, declared by `io.Seeker` or `io.ReaderAt`, and only some implementations provide it: `*os.File` on a regular file, `*bytes.Reader` and `*strings.Reader` do; a network connection, an HTTP request body, an `os.Pipe` read end and a decompressing reader do not. At run time you probe with a type assertion — `if s, ok := r.(io.Seeker); ok` — and fall back to keeping the bytes you have already read if the assertion fails. Better still, if your code genuinely needs random access, say so in the signature by taking `io.ReadSeeker` or `io.ReaderAt`, so the requirement is checked at compile time instead of failing on some callers at run time.
code
go · 11 linesfunc rereadHeader(r io.Reader) error {
s, ok := r.(io.Seeker)
if !ok {
return errors.New("index: source is not seekable")
}
if _, err := s.Seek(0, io.SeekStart); err != nil {
return err
}
// ... read the header again from r
return nil
}go deeper
Be able to state that Read is the only method a reader has, so bytes it returns are consumed and gone. Know that files can go back but network streams cannot.
Explain the optional-interface pattern: the comma-ok assertion to io.Seeker or io.ReaderAt, why it can succeed and still fail on a pipe, and which standard-library types are on each side of the line.
Show you would put the requirement in the signature rather than probe for it, and that you treat any re-read buffer as a bounded resource. The failure to describe is the component that works in tests over files and breaks on a streamed body.
This is an API commitment: the narrowest interface that admits the most callers versus the one that guarantees what your algorithm needs. Own the decision explicitly, because widening a published parameter type later is easy and narrowing it breaks every caller.
## The contract is deliberately tiny ```go type Reader interface { Read(p []byte) (n int, err error) } ``` That is the whole thing. A reader fills part of your slice and tells you how many bytes it managed. It does not expose a position, it does not promise the bytes still exist anywhere, and it has no method that could put them back. "Rewinding" is not an operation the contract has — so the question is never really "why can't I rewind a reader" but "what does *this particular* reader also implement". The design is intentional. Because the interface is one method, a socket, a decompressor, a hash-and-forward wrapper, a rate limiter and a file all satisfy it identically, and code written against it works on all of them. The price is that the interface can only promise what the weakest source can do: hand you the next bytes, once. ## Random access is a different interface Two optional interfaces add it: - `io.Seeker` — `Seek(offset int64, whence int) (int64, error)` — a movable cursor. Combined with reading, that is `io.ReadSeeker`. - `io.ReaderAt` — `ReadAt(p []byte, off int64) (n int, err error)` — reads at an offset supplied per call, with no cursor at all. Who has them: | source | seekable? | |---|---| | `*os.File` on a regular file | yes | | `*bytes.Reader`, `*strings.Reader` | yes | | `*io.SectionReader` | yes (within its section) | | an HTTP request body | no | | a network connection | no | | the read end of `os.Pipe` | no | | a decompressing reader | no | | `os.Stdin` when it is a pipe | no | Note that `*os.File` is on the yes side only for regular files. The same type wrapping a pipe or a terminal returns an error from `Seek`, which is a good illustration that seekability is a property of the underlying object, not of the Go type. ## Detecting it at run time The idiomatic probe is a type assertion: ```go if s, ok := r.(io.Seeker); ok { _, err := s.Seek(0, io.SeekStart) // ... re-read from the beginning } ``` Two cautions. First, `ok` being true does not guarantee the seek will succeed — an `*os.File` over a pipe implements `Seek` and still fails — so check the error too. Second, seeking back to zero only works if you have not wrapped the source in something that buffered ahead; a buffered wrapper still holds bytes from the old position and its view goes stale when you move the file offset underneath it. ## Declaring it instead of probing for it The stronger design move is to put the requirement in the signature. If a function must read a footer, jump to an index and then read entries, it does not want an `io.Reader` at all: ```go func OpenIndex(r io.ReaderAt, size int64) (*Index, error) ``` Now the compiler enforces the requirement at every call site, the error message arrives at build time rather than at 3am, and no runtime branch has to exist for the case that cannot be served. Accept `io.Reader` when you truly stream front to back; accept `io.ReadSeeker` or `io.ReaderAt` when you address content by position. Taking the wider interface and then failing at run time on half of your callers is the worst of both. ## When the source genuinely cannot rewind Sometimes you are handed a one-shot stream and still need to look at the start twice — sniffing a format before choosing a parser, for instance. Then the only honest answer is that *you* must keep the bytes: read a bounded prefix, keep it, and give the parser a source that yields that prefix followed by the rest. The key word is bounded. "I will keep everything in case I need to re-read" turns a streaming component into one whose memory use is set by the largest input anyone ever sends, which is a availability problem waiting for its first big file. ## What an interviewer is checking That you understand interfaces in Go describe capabilities rather than kinds of thing, that you know the optional-interface type assertion idiom and its limits, and that you would rather express a requirement in a signature than discover it at run time. A candidate who says "just call Seek on it" has missed that most readers do not have the method at all.
- Your parser needs to jump around its input. What should its parameter type be?`io.ReaderAt` (plus a size), or `io.ReadSeeker` if a cursor suits the algorithm. Both push the requirement to compile time, so a caller with a network stream is told by the compiler rather than by a runtime error. Reserve plain `io.Reader` for code that really does stream once, front to back.
- The assertion to io.Seeker succeeds but the Seek call fails. How is that possible?`*os.File` implements `Seek` whatever it wraps, but the operating system rejects the seek when the descriptor is a pipe, socket or terminal. The type says the method exists; only the call can tell you the underlying object has a position. Always check the returned error, not just the assertion.
- What is the risk of the fallback "buffer everything so I can re-read"?Memory becomes a function of the largest input anyone sends, so a component that streamed safely now falls over on a big or hostile file. If you must keep a prefix, keep a bounded one and treat exceeding the bound as an error rather than growing without limit.
saying these in an interview costs you the question
- Assumes every io.Reader has a Seek method
- Says a reader can be reset by calling Read with offset zero
- Treats a successful type assertion as proof the seek will work
- Takes io.Reader then requires random access at run time
- Buffers the entire stream so it can re-read it