• English
  • Example: io.MultiWriter

    io.MultiWriter creates a single io.Writer that duplicates write operations to multiple underlying writers.

    It is useful when the same byte stream must be delivered to multiple destinations, such as writing logs to both stdout and a file, capturing audit records, or calculating checksums during streaming operations.

    However, io.MultiWriter is a byte duplication primitive, not a replication system. Writes are executed synchronously in order, failures are unisolated, and no internal buffering, rollback, or retry logic is provided.

    NOTE

    Key Takeaways

    • Sequential & Synchronous: Writes proceed sequentially on the caller's goroutine in the order provided.

    • Fail-Fast Execution: Stops immediately at the first error encountered in the chain.

    • Non-Transactional: Partial writes are not rolled back when a downstream writer fails.

    • Latency Coupling: Overall throughput is dictated by the slowest destination.

    Quick Example

    package main
    
    import (
    	"io"
    	"log"
    	"os"
    )
    
    func main() {
    	file, err := os.OpenFile(
    		"application.log",
    		os.O_CREATE|os.O_WRONLY|os.O_APPEND,
    		0644,
    	)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer file.Close()
    
    	// Direct the same log stream to stdout and file
    	writer := io.MultiWriter(os.Stdout, file)
    
    	_, err = writer.Write([]byte("server started\n"))
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

    The application writes once, and the same byte sequence is delivered to both destinations.

    How io.MultiWriter Works

    Conceptually, io.MultiWriter builds a writer chain:

                 Write(data)
                     |
                     v
              +-------------+
              | MultiWriter |
              +-------------+
                 |    |    |
                 v    v    v
                W1   W2   W3
    

    The execution logic is equivalent to:

    for _, w := range writers {
    	n, err := w.Write(p)
    	if err != nil {
    		return n, err
    	}
    	if n != len(p) {
    		return 0, io.ErrShortWrite
    	}
    }
    return len(p), nil
    

    Execution Rules

    1. Sequential Order: Writers are called strictly in the order supplied.
    2. Synchronous Execution: Runs entirely on the calling goroutine.
    3. Strict Completion: Every writer must consume the full byte slice (n == len(p)), or io.ErrShortWrite is returned.
    4. Early Termination: The first failure halts processing for all remaining writers.

    Nested MultiWriter Flattening

    The Go standard library automatically flattens nested io.MultiWriter instances:

    // Nested initialization
    writer := io.MultiWriter(
    	w1,
    	io.MultiWriter(w2, w3),
    )
    

    This avoids unnecessary wrapper layers and keeps iteration over writers simple and predictable.


    Usage Scenarios

    1. Logging to Multiple Destinations

    Send logs to both stdout and persistent disk storage within a single pipeline.

                 Logger
                    |
                    v
              MultiWriter
                 /    \
                /      \
           stdout     logfile
    
    logger := log.New(
    	io.MultiWriter(os.Stdout, file),
    	"",
    	log.LstdFlags,
    )
    

    2. Creating an Audit Trail

    Persist a copy of outgoing responses or generated reports without buffering data entirely in memory.

    func writeReport(dst io.Writer, audit io.Writer, data io.Reader) error {
    	writer := io.MultiWriter(dst, audit)
    	_, err := io.Copy(writer, data)
    	return err
    }
    

    3. Streaming Data Processing

    Process the same byte stream through multiple consumers during transfer:

    • Calculating a hash/checksum (e.g., crypto/sha256) while saving file content.
    • Saving a local archive copy while streaming output to a network connection.
    • Broadcasting data to multiple identical output sinks.
    // Example: Write payload to disk and compute SHA256 in a single pass
    hasher := sha256.New()
    mw := io.MultiWriter(fileDestination, hasher)
    _, err := io.Copy(mw, networkReader)
    

    Production Considerations

    Failure Coupling

    All destinations exist within a single failure domain. If a non-critical secondary writer fails, the operation fails as a whole:

    Client Response (HTTP)
           |
           X <-- Failure in secondary destination aborts the primary response!
    

    When destinations have different SLAs, decouple them using an asynchronous pattern (e.g., channels and background worker routines):

    Request ---> Primary Writer (HTTP Response)
       |
       +-------> Async Channel ---> Background Worker ---> Secondary Storage
    
    IMPORTANT

    Partial Write Ambiguity & Commit Invisibility io.MultiWriter provides no transactional guarantees. Consider this sequence:

    1. Writer A: Successfully accepts 100 bytes.

    2. Writer B: Fails at byte 0.

    MultiWriter.Write() returns n = 0, err = errWriterB. Because n = 0 only reflects the failing writer's result, the caller cannot determine how much data was already accepted by earlier writers. If retry logic treats n = 0 as proof that no destination accepted the data, previously successful writers may receive duplicate bytes.


    Common Mistakes

    Mistake 1: Treating MultiWriter as Data Replication

    io.MultiWriter is an inline byte distributor. It cannot handle distributed system guarantees such as consistency, durability, recovery, or failover.

    Mistake 2: Assuming Parallel Execution

    io.MultiWriter does not fan out writes concurrently. If parallel execution or latency isolation is required, build an asynchronous concurrency pattern using Go channels.

    Mistake 3: Blending Different Reliability Domains

    Combining destinations with vastly different failure characteristics (e.g., an in-memory buffer and a remote cloud API) inside one MultiWriter creates unstable cascade failures.


    Design Boundary: io.MultiWriter vs. Replication System

    io.MultiWriter solves one specific problem:

    "How can the same bytes be delivered to multiple io.Writer instances?"

    It explicitly leaves the following concerns unhandled:

    • Failure Policy: What should happen if one destination drops connection?
    • Retry Semantics: How should failed writes be safely retried?
    • State Consistency: How is atomic commit guaranteed across destinations?
    • Performance Isolation: How are slow endpoints isolated from fast endpoints?

    These are replication system concerns, not writer composition concerns. When your architecture requires retry semantics, latency isolation, or transactional commits, io.MultiWriter is no longer the appropriate abstraction.


    When Not to Use io.MultiWriter

    Situation / ProblemRecommended Alternative
    Destinations have different reliability/failure policiesSeparate asynchronous pipelines (channels / queues)
    One destination has high or unpredictable latencyWorker pool + async queue
    Writes require per-destination retry logicDedicated writer wrapper per endpoint
    Outputs require different formats or encodingsIndependent encoder pipelines
    Destinations require transactional state consistencyDistributed commit / WAL pipeline

    Summary

    io.MultiWriter is an elegant, lightweight composition tool in the Go standard library. It adds no internal buffering and performs no additional data copying during normal Write operations.

    • Strengths: Zero internal buffering, streaming-friendly, simple composition, deterministic ordering.
    • Limitations: Synchronous execution, shared failure domain, retry hazard on partial writes, no concurrency.

    In production architecture, treat io.MultiWriter purely as a writer composition primitive, not as a resilient data replication layer.