• English
  • 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

       Source Stream (io.Reader)
    
                   │  Read(p)
    
                io.Copy
    
                   │  Write(p)
    
               io.Discard
            ┌──────────────┐
            │ Accept bytes │
            │ Retain none  │
            │ Return nil   │
            └──────────────┘
    
    
             Data discarded

    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) from Write.
    • Is a stateless global sink and can be safely shared by multiple goroutines.
    • Implements io.ReaderFrom to 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.Discard determines 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

    n, err := io.Discard.Write(p)
    // n == len(p)
    // err == nil

    Quick Start

    The simplest use is to consume an io.Reader without retaining its output.

    Go

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	src := strings.NewReader("Discarding large response stream data...")
    
    	n, err := io.Copy(io.Discard, src)
    	if err != nil {
    		fmt.Printf("failed to discard stream: %v\n", err)
    		return
    	}
    
    	fmt.Printf("Discarded %d bytes.\n", n)
    }

    Output:

    Plaintext

    Discarded 39 bytes.

    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

    package main
    
    import (
    	"io"
    	"log"
    	"log/slog"
    )
    
    func main() {
    	quietLogger := log.New(io.Discard, "", 0)
    	quietLogger.Println("This log will be discarded")
    
    	quietHandler := slog.NewTextHandler(io.Discard, nil)
    	quietSlog := slog.New(quietHandler)
    	quietSlog.Info("This structured log will also be discarded")
    }

    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

    func render(w io.Writer) error {
    	_, err := fmt.Fprintln(w, "generated output")
    	return err
    }
    
    // Caller intentionally suppresses output.
    err := render(io.Discard)

    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:

    1. It reads sequentially from src.
    2. It discards the bytes returned by src.
    3. It propagates errors encountered while reading from src.

    io.Discard does not turn a failed source read into success.

    Go

    n, err := io.Copy(io.Discard, faultyReader)
    if err != nil {
    	// The source reader failed during consumption.
    	log.Printf("failed reading source: %v", err)
    }

    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

    // Conceptual behavior of io.Discard's ReadFrom:
    func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
    	// Repeatedly read from r into an internal buffer
    	// and discard the bytes.
    }

    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: ReadFrom allows io.Copy to 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.Copy may also use WriterTo on the source or other implementation-specific optimizations. The exact execution path depends on the source and Go version.
    • No zero-allocation guarantee: io.Discard guarantees 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

    package example
    
    import (
    	"bytes"
    	"io"
    	"testing"
    )
    
    func BenchmarkStreamConsumption(b *testing.B) {
    	data := make([]byte, 1024*1024)
    
    	b.SetBytes(int64(len(data)))
    	b.ResetTimer()
    
    	for i := 0; i < b.N; i++ {
    		// Start from offset 0 for every iteration.
    		src := bytes.NewReader(data)
    
    		if _, err := io.Copy(io.Discard, src); err != nil {
    			b.Fatal(err)
    		}
    	}
    }

    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

    package example
    
    import (
    	"fmt"
    	"io"
    	"net/http"
    )
    
    func fetchAndDiscard(url string) error {
    	resp, err := http.Get(url)
    	if err != nil {
    		return err
    	}
    	defer resp.Body.Close()
    
    	const maxDrainBytes = 512 * 1024
    
    	_, err = io.CopyN(io.Discard, resp.Body, maxDrainBytes)
    	if err != nil && err != io.EOF {
    		return fmt.Errorf("drain body: %w", err)
    	}
    
    	return nil
    }

    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

    io.Copy(io.Discard, resp.Body)

    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

    _, err := io.Copy(io.Discard, reader)

    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

    if seeker, ok := reader.(io.Seeker); ok {
    	_, err := seeker.Seek(1024, io.SeekCurrent)
    	// Handle err.
    }

    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

    _, err := io.Copy(io.Discard, io.LimitReader(reader, maxBytes))

    or:

    Go

    _, err := io.CopyN(io.Discard, reader, maxBytes)

    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

    strings.NewReader("")

    or:

    Go

    bytes.NewReader(nil)

    Decision Matrix

    SituationRecommended ChoiceRationale
    Suppressing output from an io.Writerio.DiscardStandard discard sink with no retained output.
    Muting log.Logger or slog in testsio.DiscardSuppresses output without special-case logging logic.
    Consuming a stream without retaining its outputio.Copy(io.Discard, src)Discards destination data while preserving source errors.
    Benchmarking stream consumptionio.DiscardRemoves destination storage from the measured operation.
    Consuming an unneeded HTTP body with a size budgetio.CopyN(io.Discard, body, limit)Places an explicit bound on body consumption.
    Storing output for inspection or retrybytes.Buffer or another storage sinkio.Discard permanently drops the output.
    Suppressing source read errorsExplicit error handlingio.Discard does not suppress source errors.
    Skipping data in a seekable sourceio.Seeker / offset-based APIDiscarding still reads and consumes the skipped bytes.
    Supplying an empty io.Readerstrings.NewReader("") / bytes.NewReader(nil)io.Discard implements io.Writer, not io.Reader.