• English
  • Example: io.Discard

    Sometimes an API requires an io.Writer, but you genuinely do not want the output.

    For example, a test may need to execute code that writes a large amount of output without printing it. A benchmark may need to consume a stream without storing the result.

    That is what io.Discard is for:

    io.Copy(io.Discard, src)

    It accepts the bytes and keeps none of them.

    Think of it as /dev/null for Go's io.Writer interface.

    io.Discard controls the destination, not the source.

    Writing to io.Discard does not stop the source from producing, reading, decompressing, or otherwise processing its data.

    Quick Example

    Consume a stream without retaining its contents:

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	src := strings.NewReader("data that we do not need")
    
    	n, err := io.Copy(io.Discard, src)
    	if err != nil {
    		fmt.Printf("consume stream: %v\n", err)
    		return
    	}
    
    	fmt.Printf("Consumed %d bytes\n", n)
    }

    Output:

    Consumed 24 bytes

    The bytes are still read from src. They are simply discarded after being written to the destination.

    This distinction is important:

    source
      │
      │ Read
      ▼
    io.Copy
      │
      │ Write
      ▼
    io.Discard
      │
      ▼
    nothing retained

    io.Discard does not make the source cheaper or faster by itself.

    Suppressing Output

    When a function accepts an io.Writer, passing io.Discard is often cleaner than adding a special "disable output" mode.

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

    The caller can explicitly suppress the output:

    if err := render(io.Discard); err != nil {
    	return err
    }

    The function does not need to know whether its output is going to a terminal, a file, a buffer, or nowhere.

    This is particularly useful in tests and benchmarks.

    Muting Loggers

    Both log.Logger and log/slog can use io.Discard as their output destination.

    package main
    
    import (
    	"io"
    	"log"
    	"log/slog"
    )
    
    func main() {
    	logger := log.New(io.Discard, "", 0)
    	logger.Println("not printed")
    
    	handler := slog.NewTextHandler(io.Discard, nil)
    	slog.New(handler).Info("also not printed")
    }

    This keeps the logging code unchanged while suppressing its output.

    For tests, this is generally preferable to redirecting output to a temporary file or conditionally disabling logging inside the code under test.

    io.Copy Still Reads the Source

    A common misunderstanding is to treat:

    io.Copy(io.Discard, src)

    as a way to skip src.

    It is not.

    io.Copy still calls src.Read until it reaches EOF or encounters an error.

    If the source performs expensive work, that work still happens:

    network response
          │
          ▼
    decompression
          │
          ▼
    io.Copy
          │
          ▼
    io.Discard

    For example, discarding an HTTP response body still requires reading the response body from the network.

    io.Discard only eliminates the destination-side storage or processing.

    Errors Are Still Propagated

    Discarding the output does not discard source errors.

    n, err := io.Copy(io.Discard, reader)
    if err != nil {
    	return fmt.Errorf("consume stream: %w", err)
    }

    If reader.Read fails, io.Copy reports that error.

    This makes io.Discard suitable when the application does not need the data but still needs to know whether consuming the stream succeeded.

    The byte count is also meaningful:

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

    n is the number of bytes successfully consumed before the operation stopped.

    io.Discard Does Not Retain the Buffer

    io.Discard does not store the bytes passed to Write.

    buf := make([]byte, 1024)
    
    n, err := io.Discard.Write(buf)

    The write succeeds with:

    n == len(buf)
    err == nil

    The discarded writer does not retain the supplied byte slice or its contents.

    This makes it useful as a sink when the output has no value after the write completes.

    io.Discard and io.ReaderFrom

    io.Discard also implements io.ReaderFrom.

    That matters when it is used as the destination of io.Copy:

    io.Copy(io.Discard, src)

    io.Copy can use destination-side ReadFrom support when the source does not provide a suitable WriterTo implementation.

    The exact execution path is an implementation detail of io.Copy and the participating types. Do not treat a particular buffer size, allocation count, or fast path as part of the io.Discard API contract.

    If allocation or throughput matters, benchmark the complete operation you actually care about.

    Benchmarking Without Destination Storage

    io.Discard is useful when a benchmark needs to consume data but does not need to retain the result.

    package example
    
    import (
    	"bytes"
    	"io"
    	"testing"
    )
    
    func BenchmarkConsume(b *testing.B) {
    	data := make([]byte, 1<<20) // 1 MiB
    
    	b.SetBytes(int64(len(data)))
    
    	for i := 0; i < b.N; i++ {
    		src := bytes.NewReader(data)
    
    		if _, err := io.Copy(io.Discard, src); err != nil {
    			b.Fatal(err)
    		}
    	}
    }

    The benchmark measures the complete bytes.Reader → io.Copy → io.Discard operation.

    It does not measure "pure reader performance."

    That distinction matters when comparing implementations. If the real application consumes a network stream, compressed data, or a file, benchmark that actual path instead of assuming io.Discard removes all unrelated work.

    Bounded Consumption

    Sometimes the application does not need the data, but it also should not consume an unlimited amount of it.

    For example:

    _, err := io.Copy(
    	io.Discard,
    	io.LimitReader(reader, maxBytes),
    )
    if err != nil {
    	return err
    }

    This places a maximum on how many bytes the application will consume.

    io.CopyN is another option:

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

    The choice depends on the desired semantics.

    The important point is:

    Discarding output and limiting input are separate concerns.

    io.Discard answers:

    Where should the output go?

    io.LimitReader answers:

    How much input should this reader expose?

    Neither one provides a timeout or cancellation policy for a blocking network operation.

    HTTP Response Bodies

    io.Discard is sometimes useful when an HTTP response body is not needed:

    resp, err := http.Get(url)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()
    
    if _, err := io.Copy(io.Discard, resp.Body); err != nil {
    	return fmt.Errorf("consume response body: %w", err)
    }

    But this makes an important commitment:

    Consume the body until EOF or error.

    For a large or untrusted response, that may be the wrong choice.

    If the application only wants to consume a bounded amount:

    const maxDrain = 512 << 10 // 512 KiB
    
    _, err = io.CopyN(io.Discard, resp.Body, maxDrain)
    if err != nil && err != io.EOF {
    	return fmt.Errorf("drain response body: %w", err)
    }

    A byte limit does not guarantee a time limit. Network operations still need appropriate timeout or cancellation policies.

    Also, io.Discard itself has no HTTP connection-management semantics. Whether a connection can be reused depends on the HTTP transport and how the response body is handled.

    Common Mistakes

    Mistake 1: Treating discard as skip

    This:

    io.Copy(io.Discard, reader)

    still reads the data.

    If the source supports seeking and the application genuinely wants to move past bytes without reading them, use the source's seeking or offset-based API instead.

    Mistake 2: Assuming discard removes source-side cost

    If the source performs:

    • network I/O
    • decompression
    • decryption
    • parsing
    • database reads

    that work still happens.

    io.Discard only removes the need to retain or process the resulting bytes at the destination.

    Mistake 3: Ignoring source errors

    This is still wrong:

    io.Copy(io.Discard, reader)

    when successful consumption matters.

    Check the returned error:

    if _, err := io.Copy(io.Discard, reader); err != nil {
    	return err
    }

    Mistake 4: Using an unbounded drain when a limit is required

    This:

    io.Copy(io.Discard, reader)

    means "consume until EOF or error."

    If the input is untrusted or potentially very large, establish an explicit size policy when appropriate.

    Mistake 5: Using io.Discard when the output will be needed later

    Once written to io.Discard, the bytes are gone.

    If the caller needs to inspect, retry, or process the output later, use an appropriate storage or processing writer instead.

    io.Discard vs Other Choices

    NeedChoice
    Permanently discard writer outputio.Discard
    Store output in memorybytes.Buffer
    Observe bytes while passing them throughio.TeeReader
    Send output to multiple writersio.MultiWriter
    Consume only up to a byte limitio.LimitReader
    Skip bytes in a seekable sourceio.Seeker / offset-based API

    The key distinction is that io.Discard is a destination.

    It does not control how the input is produced or how much input should be consumed.

    Rule of Thumb

    Use io.Discard when an API needs an io.Writer and the output is intentionally unwanted.

    And remember:

    Discarding output is not the same as skipping input.