• English
  • Example: io.SectionReader

    You have a large file and need to process only one byte range without loading the entire file into memory.

    io.SectionReader gives you a bounded, seekable view of that range.

    section := io.NewSectionReader(file, offset, length)

    The important idea is:

    io.SectionReader creates a bounded view. It does not copy the data.

    Quick Example

    Read bytes 10 through 14 from a larger source:

    package main
    
    import (
    	"fmt"
    	"io"
    	"log"
    	"strings"
    )
    
    func main() {
    	r := strings.NewReader("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    
    	section := io.NewSectionReader(r, 10, 5)
    
    	data, err := io.ReadAll(section)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	fmt.Println(string(data))
    	// ABCDE
    }

    The section starts at offset 10 in the underlying reader and exposes 5 bytes:

    underlying data:
    0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
              ^^^^^
              ABCDE
    
    section:
    ABCDE

    io.ReadAll handles io.EOF internally, so the example does not need special EOF handling.

    When using Read directly, remember that io.EOF normally means the section has no more bytes to return. It is not necessarily an error condition.

    What SectionReader Provides

    A SectionReader has three important properties:

    • bounded — it cannot read beyond the specified section
    • seekable — it supports Seek
    • random-access capable — it supports ReadAt

    The section is defined by:

    io.NewSectionReader(r, offset, length)

    where:

    offset
      |
      v
      +-------------------+
      |   selected bytes  |
      +-------------------+
      <------ length ----->

    The underlying reader must implement io.ReaderAt.

    The section's offsets are relative to the section, not the underlying source.

    For example:

    file:
    0         10        20
    |---------|---------|
              ^^^^^
              section
    
    section offset 0 -> file offset 10
    section offset 5 -> file offset 15

    This makes a section behave like an independent byte range.

    Read and ReadAt Use Different Positions

    Read maintains the section's current position:

    buf := make([]byte, 4)
    
    n, err := section.Read(buf)

    Each successful read advances that position.

    ReadAt instead uses an explicit offset:

    n, err := section.ReadAt(buf, 8)

    The offset 8 is relative to the section.

    It does not change the section's current position.

    This distinction is useful when code needs both sequential processing and explicit chunk access.

    The Section Is Not a Copy

    Creating a section does not read the selected bytes into memory.

    section := io.NewSectionReader(file, 1<<30, 1<<20)

    This does not allocate a 1 MiB buffer containing the section.

    Instead, the SectionReader keeps the boundaries and translates operations to the underlying ReaderAt.

    That makes it useful for large files, where copying each requested region would be unnecessary.

    Concurrent Chunk Processing

    A common use case is processing different regions of a large file concurrently.

    Each worker can receive its own SectionReader:

    func processChunk(r io.Reader) error {
    	_, err := io.Copy(io.Discard, r)
    	return err
    }

    For example:

    package main
    
    import (
    	"io"
    	"log"
    	"os"
    	"sync"
    )
    
    func processChunk(r io.Reader) error {
    	_, err := io.Copy(io.Discard, r)
    	return err
    }
    
    func main() {
    	file, err := os.Open("large.dat")
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer file.Close()
    
    	info, err := file.Stat()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	const chunkSize int64 = 1 << 20 // 1 MiB
    
    	var wg sync.WaitGroup
    
    	for offset := int64(0); offset < info.Size(); offset += chunkSize {
    		length := min(chunkSize, info.Size()-offset)
    		section := io.NewSectionReader(file, offset, length)
    
    		wg.Add(1)
    		go func() {
    			defer wg.Done()
    
    			if err := processChunk(section); err != nil {
    				log.Printf("process chunk: %v", err)
    			}
    		}()
    	}
    
    	wg.Wait()
    }

    Each goroutine gets a different view:

    large.dat
    +---------+---------+---------+---------+
    | chunk 0 | chunk 1 | chunk 2 | chunk 3 |
    +---------+---------+---------+---------+
         |         |         |         |
         v         v         v         v
       worker    worker    worker    worker

    The important point is not that SectionReader makes a reader magically concurrent.

    The point is that each section has its own logical position, while the underlying ReaderAt provides the random-access operation.

    For example, *os.File supports concurrent ReadAt operations.

    SectionReader itself is not a general-purpose synchronization mechanism. Sharing one SectionReader between concurrent sequential Read operations requires synchronization.

    SectionReader Does Not Own the Source

    Creating a section does not transfer ownership:

    file, err := os.Open("large.dat")
    if err != nil {
    	return err
    }
    defer file.Close()
    
    section := io.NewSectionReader(file, 100, 500)

    Closing the file closes the underlying resource.

    There is no separate Close on SectionReader.

    This matters when designing APIs: the code that opens the file should normally remain responsible for its lifetime.

    SectionReader vs LimitReader

    These two APIs both restrict reads, but they solve different problems.

    APIMain purpose
    io.LimitReaderLimit how many bytes a sequential reader exposes
    io.SectionReaderExpose a fixed, seekable range from a random-access source

    With LimitReader:

    limited := io.LimitReader(r, 1024)

    you start reading from the source's current position.

    With SectionReader:

    section := io.NewSectionReader(r, 4096, 1024)

    you explicitly select a byte range.

    Use LimitReader when you need a byte limit.

    Use SectionReader when you need a specific region of a random-access source.

    Common Mistakes

    Mistake 1: Treating the section as a copy

    section := io.NewSectionReader(file, offset, length)

    does not copy the data.

    The underlying source is still accessed when the section is read.

    Mistake 2: Expecting reads to continue past the section

    If the section contains 100 bytes, reading the 101st byte does not continue into the rest of the file.

    The boundary is part of the abstraction.

    Mistake 3: Sharing one SectionReader between workers

    A SectionReader has a current position.

    This is problematic:

    go processChunk(section)
    go processChunk(section)

    if both workers perform sequential Read operations on the same instance.

    Create separate sections when workers need independent positions.

    Mistake 4: Assuming SectionReader closes the file

    It does not own the underlying resource.

    The original ReaderAt remains responsible for its lifetime.

    Mistake 5: Using it with a purely sequential source

    SectionReader requires io.ReaderAt.

    If the source only supports sequential reads, such as a network stream, SectionReader is not the right abstraction.

    Rule of Thumb

    Use io.SectionReader when you need a fixed, seekable view over a byte range of a random-access source.

    It gives you bounded access without copying the underlying data, and separate sections can be used to process different ranges independently.