Example: io.Copy
io.Copy transfers bytes from an io.Reader to an io.Writer until the source reaches io.EOF or an error occurs.
It is the default choice when data should be streamed rather than materialized in memory.
Quick Example
The argument order is:
Think to destination, from source.
On success, io.Copy returns err == nil. The source's io.EOF is consumed internally rather than returned as an error.
The returned byte count is the number of bytes successfully written to dst.
Copy Without Materializing the Input
A common use is transferring a large file:
The same pattern works with HTTP bodies, network connections, compressed streams, pipes, and other io.Reader / io.Writer implementations.
The important property is that the application does not need to hold the complete payload in memory.
Optimized Transfer Paths
io.Copy does not always use a generic read/write loop.
Its current implementation checks the source for io.WriterTo first:
Only when the source does not implement WriterTo does it check the destination for io.ReaderFrom:
The precedence is therefore:
If both interfaces are available, WriterTo wins.
These interfaces allow concrete implementations to provide specialized transfer paths. On supported platforms and I/O combinations, those paths can also take advantage of kernel-assisted or zero-copy mechanisms such as Linux sendfile or copy_file_range.
This is an important reason not to replace io.Copy with a hand-written buffering loop prematurely. The generic API can already expose implementation-specific optimizations underneath.
io.CopyBuffer
Use io.CopyBuffer when you have a concrete reason to provide and reuse a buffer:
The supplied buffer is used only by the generic copy path.
If src implements WriterTo or dst implements ReaderFrom, the optimized path can bypass the buffer entirely.
Passing nil behaves like io.Copy:
The same WriterTo → ReaderFrom detection still applies, and the generic path uses its standard internal buffer.
Do not use CopyBuffer merely because a particular buffer size looks faster. Measure first.
Exact vs Bounded Copy
io.CopyN and io.LimitReader solve different problems.
Use io.CopyN when exactly N bytes must be transferred:
If the source ends before 1024 bytes are copied, io.CopyN returns io.EOF.
io.LimitReader instead exposes at most N bytes:
This distinction becomes useful when parsing untrusted protocol data.
For example, suppose a protocol declares a payload length but the server also imposes a maximum:
Here the two operations enforce different contracts:
- the maximum prevents an untrusted length from causing excessive resource consumption;
CopyNverifies that the declared number of bytes actually arrives.
This pattern separates resource protection from protocol validation.
Common Mistakes
1. Forgetting to Close Resources
io.Copy does not close either the source or destination.
Resource ownership remains with the caller:
This matters for files, HTTP bodies, temporary files, network connections, and other resources with an explicit lifecycle.
io.Copytransfers data; it does not manage resource ownership.
2. Reading Everything Before Copying
Avoid this for large or unbounded input:
This materializes the entire input in memory.
Prefer:
3. Expecting Rollback on Error
io.Copy is not transactional.
If 50 MB are written before an error occurs, those 50 MB remain in the destination:
If the source size is known—for example from Content-Length or os.File.Stat—including it in diagnostics can help distinguish a partial transfer from a truncated source.
If the destination must only become visible after a complete successful copy, use an atomic-write pattern:
The key idea is not that io.Copy becomes atomic. The application makes the destination update atomic by publishing the completed temporary file only after the copy succeeds.
4. Assuming io.Copy Provides Timeouts
io.Copy itself has no timeout or cancellation mechanism.
If the underlying reader or writer can block indefinitely, configure deadlines or cancellation at the I/O layer.
For example, network connections should normally have appropriate deadlines managed outside the copy loop.
5. Assuming a Fixed Buffer Size
Do not depend on a particular internal buffer size.
io.Copy may use WriterTo or ReaderFrom instead of the generic buffered loop, and implementation details can vary across Go versions and platforms.
If buffer ownership or reuse is actually important, use io.CopyBuffer deliberately.
What io.Copy Does Not Do
io.Copy deliberately keeps its contract small.
It does not:
- retry failed transfers
- roll back partial output
- close either side
- impose a timeout
- enforce an input limit
- verify application-level checksums
- make the destination atomic
Those responsibilities belong to the surrounding code or protocol.
Rule of Thumb
If you need to transfer a stream from an
io.Readerto anio.Writer, start withio.Copy.
Use:
io.CopyNwhen an exact byte count is required.io.LimitReaderwhen the amount of input must be bounded.io.CopyBufferwhen you have a concrete reason to control or reuse the buffer.
And before writing a manual high-performance copy loop, let io.Copy try its optimized paths first.