Example: io.Discard
io.Discard is a global, stateless io.Writer that accepts all writes and discards their contents.
It is conceptually similar to /dev/null for output streams. It is useful when an API requires an io.Writer, but the produced data is intentionally unwanted.
Plaintext
Key Characteristics
io.Discard:
- Acts as a discard sink with no per-write data storage.
- Does not retain written data or keep references to supplied byte slices.
- Returns
(len(p), nil)fromWrite. - Is a stateless global sink and can be safely shared by multiple goroutines.
- Implements
io.ReaderFromto provide a specialized stream-draining path.
Write itself is extremely simple: it does not inspect or retain the supplied bytes and returns (len(p), nil). ReadFrom is a separate optimization for consuming an io.Reader through io.Copy.
Core Semantics
io.Discarddetermines writer-side behavior. It controls what happens to bytes delivered to the destination, but it does not change how the source produces those bytes. Source reads, blocking I/O, and source errors remain intact.
Go
Quick Start
The simplest use is to consume an io.Reader without retaining its output.
Go
Output:
Plaintext
The source is still fully consumed. io.Discard only determines what happens to the bytes after they are read.
Muting Loggers in Tests and Benchmarks
A common requirement in testing is suppressing log output to keep test results clean or prevent logging I/O from affecting benchmark measurements. Both log.Logger and log/slog can write to io.Discard.
Go
This keeps the logging API unchanged while allowing the caller to suppress the resulting output.
Writing Directly to io.Discard
When a function accepts an io.Writer, callers can pass io.Discard when the output is intentionally unwanted.
Go
This avoids adding special-case output branches inside the function itself.
io.Discard and io.Copy
Using io.Copy(io.Discard, src) has three important effects:
- It reads sequentially from
src. - It discards the bytes returned by
src. - It propagates errors encountered while reading from
src.
io.Discard does not turn a failed source read into success.
Go
The same distinction matters for network streams: discarding a response body does not make network timeouts, connection failures, or other source errors disappear.
Fast Path: io.ReaderFrom
io.Copy first checks whether the source implements io.WriterTo. If that path is unavailable, it checks whether the destination implements io.ReaderFrom.
io.Discard implements io.ReaderFrom, allowing it to provide a specialized path for consuming a source when io.Copy selects that strategy.
Go
Implementation Details vs. API Contracts
The important distinction is between the public API contract and the implementation used by a particular Go release.
- Interface collaboration:
ReadFromallowsio.Copyto delegate stream consumption to the destination rather than requiring the caller to implement a read/write loop. - Internal optimization: The standard library controls temporary buffer management and other implementation details.
- Fast-path selection:
io.Copymay also useWriterToon the source or other implementation-specific optimizations. The exact execution path depends on the source and Go version. - No zero-allocation guarantee:
io.Discardguarantees discard semantics, not a universal "zero allocations on every call path" property.
Therefore, allocation behavior should be verified with benchmarks rather than treated as part of the io.Discard API contract.
Benchmarking Stream Consumption
io.Discard is useful when a benchmark needs to consume a stream without retaining the destination bytes.
It does not automatically isolate the source reader from io.Copy, bytes.Reader, or other surrounding work. A useful benchmark should model the actual source and operation whose cost is being measured.
Go
The destination does not retain the 1 MiB payload, so the benchmark avoids measuring destination-buffer growth or storage.
However, the benchmark still measures the complete io.Copy path, including bytes.Reader and io.Discard. If the goal is to compare reader implementations, benchmark the complete operation that matters rather than assuming io.Discard makes the result "pure reader throughput."
io.Discard for HTTP Body Consumption
io.Discard is useful when an HTTP response body must be consumed but its contents are not needed.
However, io.Discard has no HTTP connection-management semantics. Whether consuming a body enables connection reuse depends on the HTTP transport, protocol, response state, and how the body is handled.
If the application intentionally wants to limit how much unread data it will consume, it can impose an explicit drain budget:
Go
io.CopyN provides a consumption budget. It does not determine whether the response body is larger than that budget.
If exactly maxDrainBytes are consumed, the body may have ended at that boundary or may contain additional unread data.
Unbounded Draining Pitfalls
Blindly calling:
Go
means the application has chosen to consume the response body until EOF or error.
That can be inappropriate for large, untrusted, or intentionally streaming responses because it may result in:
- Excessive bandwidth consumption
- Unbounded latency
- Increased CPU and network resource consumption
- Prolonged blocking while waiting for a slow or non-terminating source
If the remaining response body is too large, too slow, or otherwise not worth consuming, closing resp.Body without an unbounded drain may be preferable.
Critical Production Pitfalls
1. Source-Side Work Still Happens
io.Discard does not stop the source from doing work.
Go
still causes reader.Read to execute repeatedly until EOF or an error occurs.
If the reader performs network I/O, decompression, decryption, or other expensive processing, that work still happens.
2. Discarding Is Not Skipping
io.Copy(io.Discard, reader) consumes the stream sequentially.
If the source supports random access and the application wants to move past data without reading it, use an appropriate seeking or offset-based API instead.
Go
Whether seeking is appropriate depends on the source and its semantics.
3. Avoid Unbounded Stream Consumption
If an application consumes data that it does not need, impose an explicit size or time policy where appropriate.
For size-bounded consumption:
Go
or:
Go
The choice depends on the desired semantics.
A byte limit alone does not provide a timeout. Network operations should also have an appropriate cancellation or deadline policy.
4. io.Discard Is an io.Writer, Not an io.Reader
io.Discard cannot be used where an io.Reader is required.
For an empty reader, use an actual reader such as:
Go
or:
Go