Understanding io.EOF in Go
io.EOF is one of the most commonly misunderstood values in Go.
It is an error, but reaching io.EOF does not usually mean something went wrong.
The key question is:
Was reaching the end of the input valid for this operation?
Once you understand that distinction, io.EOF becomes much easier to reason about.
Quick Example
A Reader may return data first and io.EOF later:
Output:
The final read reaches the end of the input.
That is not a failure. It is how the reader tells the caller:
There is no more data.
What Does io.EOF Actually Mean?
io.EOF means:
The reader has no more input available.
It does not inherently mean:
- the operation failed
- the input is invalid
- a resource was closed
- a network connection was closed
- a protocol message is complete
- the input is valid
Those are separate questions.
io.EOF only describes the state of the input stream.
Think of it as a stream boundary, not automatically as an error condition.
io.EOF Is a Sentinel Error
io.EOF is a sentinel error: a predefined package-level error value used to represent a specific condition.
The standard library defines it as:
Because it is a specific shared value, direct comparison works:
If an error wraps io.EOF, direct comparison no longer works:
Use errors.Is when wrapped errors are possible:
Also note:
The important point is that io.EOF is an actual sentinel value, not a special error type that callers need to type-assert.
io.EOF Is an Error Value, Not Usually an Error Condition
Go uses the error interface to report many different conditions.
Some of those conditions represent failures.
Others represent normal control flow.
io.EOF belongs to the second category in many common reader operations.
For example:
Here, EOF means the loop has completed normally.
The important distinction is:
io.EOF is an error value, but reaching the end of input is often expected.
The Most Important Rule: Process n Before err
This is the rule that prevents many EOF-related bugs.
A Reader returns two pieces of information:
You must process the bytes represented by n before interpreting err.
A reader can legally return data together with an error:
For example:
Do not write code that assumes an error means no data was returned:
If the reader returns:
the code above loses the five bytes.
This also matters when using buffered readers such as bufio.Reader: a read can return data together with io.EOF.
The general rule is:
Process
nfirst. Then processerr.
Usually, You Should Not Handle EOF Yourself
Many higher-level I/O functions already interpret normal EOF for you.
io.Copy
Consider:
You do not normally write:
io.Copy treats io.EOF from the source as normal completion.
A successful io.Copy returns nil, not io.EOF.
Other source read errors are returned to the caller rather than being treated as successful completion. This includes errors such as io.ErrUnexpectedEOF and network errors.
So:
The EOF has already been consumed as part of the copy operation.
io.ReadAll
io.ReadAll follows the same basic idea:
It reads until the reader reaches EOF.
Normal EOF is treated as successful completion:
If the reader returns another error, io.ReadAll returns the data accumulated so far together with that error.
For example:
Do not use data == nil as a generic test for whether the input contained data.
If you need to know whether any bytes were returned, use:
The important semantic fact is the length of the returned data, not whether the slice happens to be nil.
EOF vs io.ErrUnexpectedEOF
This distinction becomes important when the caller expects a specific amount of data.
Suppose a protocol defines an 8-byte header.
You can use:
There are two important cases.
No bytes are available
If the reader immediately reaches EOF:
The input simply ended before any header bytes arrived.
Some bytes were read
If only part of the header arrives:
This means the input ended unexpectedly while a fixed-size value was still incomplete.
That distinction is extremely useful in protocol parsing:
The same underlying stream boundary can therefore have different meanings depending on the operation.
EOF Does Not Close Resources
EOF and resource lifetime are separate concepts.
For example:
The reader reaching EOF does not close the file.
You still need:
Likewise, with HTTP:
io.Copy consumes EOF from resp.Body as normal completion.
Close is a separate resource-lifecycle operation.
For HTTP clients, consuming the response body and then closing it also matters for connection reuse.
If a response body is closed before it is fully consumed, the underlying connection may not be reusable and may need to be discarded.
When the response data is not needed, draining it with:
before Close is a common pattern when connection reuse matters.
The important mental model is:
They are not interchangeable.
EOF Does Not Define Protocol Semantics
EOF belongs to the I/O layer.
A protocol decides what EOF means.
For one protocol:
For another:
For a fixed-length structure:
For a stream where EOF itself terminates the message:
Therefore, code should not blindly translate:
into:
The correct question is:
Was EOF an acceptable boundary for this operation?
EOF Is Different from Cancellation and Timeout
EOF, cancellation, and timeout represent different conditions.
For example:
These conditions should not be collapsed into one category:
A cancellation is not equivalent to a clean end-of-input condition.
In context-aware network or application code, cancellation may cause an underlying operation to stop, but the application should preserve the cancellation semantics rather than arbitrarily converting the result into EOF.
When Should You Use errors.Is?
For a directly returned reader error, this is idiomatic:
Use errors.Is when the error may be wrapped:
For example:
can wrap an underlying error.
A practical guideline:
If you consume the error immediately in the same function where it was produced,
==is usually sufficient. If the error may be wrapped before reaching its final handler, prefererrors.Is.
For io.EOF specifically, there is an additional detail worth knowing: the io package documentation requires Reader implementations to return io.EOF itself rather than an error wrapping io.EOF, because callers may compare it with ==.
Common Mistakes
1. Treating EOF as a Failure
Avoid
This may incorrectly treat normal EOF as a failure.
Instead
Or, when appropriate, let io.Copy or io.ReadAll handle EOF for you.
2. Returning Before Processing n
Avoid
A reader may return:
Instead
Always process the bytes first.
3. Assuming EOF Means the Connection Was Closed
Avoid
EOF only tells you that the reader has no more input.
It does not, by itself, tell you why the stream ended or what happened to the underlying resource.
Instead
Interpret EOF at the appropriate abstraction level.
For example, an HTTP response body reaching EOF means the body has no more data. It does not by itself mean the underlying connection was closed.
4. Using EOF to Validate Fixed-Size Input
Avoid
A single Read is not a reliable way to obtain a fixed number of bytes.
Instead
Now a truncated header is represented by io.ErrUnexpectedEOF.
5. Forgetting That io.ReadAll Can Return Partial Data
Avoid
This may discard useful data that was successfully read before the error.
Instead
Whether partial data is useful depends on the caller and the protocol.
6. Treating Cancellation as EOF
Avoid
This collapses two different conditions into the same result.
A caller may need to know whether the stream ended normally or the operation was canceled.
Instead
Preserve cancellation semantics unless the surrounding API explicitly defines cancellation as normal completion.
7. Handling EOF at Every Layer
Avoid
io.Copy already treats a normal source EOF as successful completion.
The helper does not need to translate it into another application-level error.
Instead
Handle EOF at the layer that actually needs to assign meaning to the end of the stream.
The Production Mental Model
When working with a Reader, think in terms of:
For fixed-length input:
The key rules are:
- Process
nbeforeerr. io.EOFmeans no more input.io.EOFis not automatically a failure.- Higher-level I/O functions often consume normal EOF for you.
- Other read errors are not equivalent to EOF.
- Fixed-length operations may interpret EOF as truncation.
- EOF does not close resources.
- Cancellation and timeout are different conditions.
The final question to ask is always:
Was reaching the end of the input valid for this operation?
That question is more useful than memorizing "io.EOF means end of file."
In Go, io.EOF is best understood as a stream boundary whose meaning is determined by the operation consuming it.