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

    package main
    
    import (
    	"io"
    	"log"
    	"os"
    	"strings"
    )
    
    func main() {
    	src := strings.NewReader("Hello, io.Copy from gobase.net.\n")
    
    	if _, err := io.Copy(os.Stdout, src); err != nil {
    		log.Fatal(err)
    	}
    }

    The argument order is:

    io.Copy(dst, src)

    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:

    func copyFile(srcPath, dstPath string) error {
    	src, err := os.Open(srcPath)
    	if err != nil {
    		return err
    	}
    	defer src.Close()
    
    	dst, err := os.Create(dstPath)
    	if err != nil {
    		return err
    	}
    	defer dst.Close()
    
    	_, err = io.Copy(dst, src)
    	return err
    }

    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:

    if wt, ok := src.(io.WriterTo); ok {
    	return wt.WriteTo(dst)
    }

    Only when the source does not implement WriterTo does it check the destination for io.ReaderFrom:

    if rt, ok := dst.(io.ReaderFrom); ok {
    	return rt.ReadFrom(src)
    }

    The precedence is therefore:

    src implements WriterTo
            ↓
        use it
    
    otherwise
    
    dst implements ReaderFrom
            ↓
        use it
    
    otherwise
    
    generic buffered copy

    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:

    buf := make([]byte, 64*1024)
    
    _, err := io.CopyBuffer(dst, src, buf)
    if err != nil {
    	return err
    }

    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:

    _, err := io.CopyBuffer(dst, src, nil)

    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:

    written, err := io.CopyN(dst, src, 1024)
    if err != nil {
    	return err
    }

    If the source ends before 1024 bytes are copied, io.CopyN returns io.EOF.

    io.LimitReader instead exposes at most N bytes:

    limited := io.LimitReader(src, 1024)
    
    _, err := io.Copy(dst, limited)
    if err != nil {
    	return err
    }

    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:

    const maxPayload = 1 << 20 // 1 MiB
    
    if declaredLen > maxPayload {
    	return fmt.Errorf("payload too large: %d", declaredLen)
    }
    
    payload := io.LimitReader(src, declaredLen)
    
    written, err := io.CopyN(dst, payload, declaredLen)
    if err != nil {
    	return fmt.Errorf("payload truncated after %d bytes: %w", written, err)
    }

    Here the two operations enforce different contracts:

    • the maximum prevents an untrusted length from causing excessive resource consumption;
    • CopyN verifies 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:

    src, err := os.Open(srcPath)
    if err != nil {
    	return err
    }
    defer src.Close()
    
    dst, err := os.Create(dstPath)
    if err != nil {
    	return err
    }
    defer dst.Close()
    
    if _, err := io.Copy(dst, src); err != nil {
    	return err
    }

    This matters for files, HTTP bodies, temporary files, network connections, and other resources with an explicit lifecycle.

    io.Copy transfers data; it does not manage resource ownership.

    2. Reading Everything Before Copying

    Avoid this for large or unbounded input:

    data, err := io.ReadAll(src)
    if err != nil {
    	return err
    }
    
    _, err = dst.Write(data)
    return err

    This materializes the entire input in memory.

    Prefer:

    _, err := io.Copy(dst, src)
    return err

    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:

    written, err := io.Copy(dst, src)
    if err != nil {
    	return fmt.Errorf("copy failed after %d bytes: %w", written, err)
    }

    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:

    func copyFileAtomic(srcPath, dstPath string) error {
    	tmpPath := dstPath + ".tmp"
    
    	src, err := os.Open(srcPath)
    	if err != nil {
    		return err
    	}
    	defer src.Close()
    
    	tmp, err := os.Create(tmpPath)
    	if err != nil {
    		return err
    	}
    
    	if _, err := io.Copy(tmp, src); err != nil {
    		tmp.Close()
    		os.Remove(tmpPath)
    		return err
    	}
    
    	if err := tmp.Close(); err != nil {
    		os.Remove(tmpPath)
    		return err
    	}
    
    	if err := os.Rename(tmpPath, dstPath); err != nil {
    		os.Remove(tmpPath)
    		return err
    	}
    
    	return nil
    }

    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.Reader to an io.Writer, start with io.Copy.

    Use:

    • io.CopyN when an exact byte count is required.
    • io.LimitReader when the amount of input must be bounded.
    • io.CopyBuffer when 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.