• English
  • Example: io.SectionReader

    io.SectionReader provides a bounded, seekable view over an io.ReaderAt.

    Constructed with io.NewSectionReader(r, off, n), it exposes a logical byte range of length n starting at absolute offset off in the underlying source.

    The section's read boundary is fixed: callers can seek within or beyond the logical section, but Read and ReadAt never return bytes outside [off, off+n).

    Plaintext

                     io.ReaderAt (e.g., *os.File)
    
                   explicit offset reads
    
               ┌──────────┼──────────┐
               ▼          ▼          ▼
           Section A  Section B  Section C
           [0, 10)    [10, 20)   [20, 30)
               │          │          │
            cursor     cursor     cursor
               │          │          │
            worker     worker     worker

    Key Characteristics

    io.SectionReader does not:

    • Allocate a separate copy of the section data
    • Buffer input bytes
    • Mutate the underlying source's shared read offset
    • Own, close, or extend the lifetime of the underlying io.ReaderAt

    It maintains its own sequential cursor while using explicit ReadAt calls against the underlying source.

    Common Use Cases

    • Concurrent chunk processing: Give each worker its own SectionReader so workers can process different regions of the same file without synchronizing a shared file offset.

    • Parsing binary container formats: Expose headers or embedded payloads such as ZIP, ELF, or MP4 regions as independent seekable readers.

    • Bounded random access: Restrict reads to a known byte range while retaining ReadAt and Seek capabilities.

    Core Semantics

    io.SectionReader separates two concepts:

    1. A fixed section boundary defined by base and size.
    2. A mutable sequential cursor used by Read and Seek.

    ReadAt does not use or modify that sequential cursor. It performs a relative random-access read within the section and translates the relative offset into an absolute offset for the underlying ReaderAt.

    A single *io.SectionReader has mutable state and should not be concurrently used by multiple goroutines. Multiple independent SectionReader instances can safely operate on the same ReaderAt when that ReaderAt supports concurrent ReadAt calls, as *os.File does.

    Quick Start & Relative Offset Semantics

    io.SectionReader presents positions relative to the section.

    • Read starts at relative offset 0 and advances the internal cursor.
    • ReadAt accepts a relative offset and does not modify the sequential cursor.
    • Seek changes the sequential cursor using the section's logical coordinate space.

    Go

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	// Source data: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
    	src := strings.NewReader("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    
    	// Section: absolute [10, 15) => "ABCDE"
    	sec := io.NewSectionReader(src, 10, 5)
    
    	// 1. Read sequentially from relative offset 0.
    	buf := make([]byte, 3)
    	n, err := sec.Read(buf)
    	if err != nil && err != io.EOF {
    		fmt.Printf("read failed: %v\n", err)
    		return
    	}
    	fmt.Printf("Read 3 bytes: %s\n", buf[:n])
    
    	// 2. ReadAt uses a relative offset and does not change the
    	// sequential cursor.
    	atBuf := make([]byte, 2)
    	n, err = sec.ReadAt(atBuf, 2)
    	if err != nil && err != io.EOF {
    		fmt.Printf("readat failed: %v\n", err)
    		return
    	}
    	fmt.Printf("ReadAt relative offset 2: %s\n", atBuf[:n])
    
    	// 3. The sequential cursor is still at relative offset 3.
    	nextBuf := make([]byte, 2)
    	n, err = sec.Read(nextBuf)
    	if err != nil && err != io.EOF {
    		fmt.Printf("read failed: %v\n", err)
    		return
    	}
    	fmt.Printf("Next sequential Read: %s\n", nextBuf[:n])
    
    	fmt.Printf("Section size: %d bytes\n", sec.Size())
    }

    Output:

    Read 3 bytes: ABC
    ReadAt relative offset 2: CD
    Next sequential Read: DE
    Section size: 5 bytes

    The important point is that ReadAt and Read maintain separate notions of position:

    Section: [A B C D E]
              0 1 2 3 4   relative offsets
    
    Read(3)
      └── sequential cursor: 0 → 3
    
    ReadAt(..., 2)
      └── reads from offset 2
      └── sequential cursor remains 3
    
    Read(2)
      └── reads from offset 3 → "DE"
      └── sequential cursor: 3 → 5

    Relative ReadAt Translation

    Underlying Source
    
    0                   10                    15
    |-------------------|=====================|-------------------|
                        Section [10, 15)
    
    sec.ReadAt(buf, 2)
    
    
    underlying.ReadAt(buf, 10 + 2)
    
    
                        Absolute offset 12

    The caller works entirely in section-relative offsets. SectionReader performs the translation to the underlying source's absolute address space.

    Recovering the Underlying Source with Outer

    Outer was added to io.SectionReader in Go 1.22.

    It returns the original io.ReaderAt, the section's starting offset, and the original section length supplied to NewSectionReader.

    Go

    package main
    
    import (
    	"fmt"
    	"io"
    	"strings"
    )
    
    func main() {
    	src := strings.NewReader("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    	sec := io.NewSectionReader(src, 10, 5)
    
    	r, off, n := sec.Outer()
    
    	fmt.Printf("Base offset: %d\n", off)
    	fmt.Printf("Length: %d\n", n)
    	fmt.Printf("Source: %T\n", r)
    }

    Output:

    Base offset: 10
    Length: 5
    Source: *strings.Reader

    Outer exposes the construction parameters. It does not expose or modify the current sequential cursor.

    This is useful when a higher-level component needs to inspect or reconstruct the original section relationship without relying on implementation-specific fields.

    Concurrent Chunk Processing

    A shared *os.File has a mutable sequential file offset. Concurrent Read calls on that shared stream therefore require coordination when workers depend on independent positions.

    io.SectionReader avoids that shared-offset problem by using ReaderAt.ReadAt, which takes an explicit offset.

    The important concurrency pattern is:

                        *os.File
    
                     concurrent ReadAt
    
              ┌────────────┼────────────┐
              ▼            ▼            ▼
          Section A    Section B    Section C
          [0,10)       [10,20)      [20,30)
              │            │            │
           worker 0      worker 1     worker 2

    Each worker should have its own SectionReader.

    Go

    package main
    
    import (
    	"fmt"
    	"io"
    	"os"
    	"sync"
    )
    
    func main() {
    	file, err := os.CreateTemp("", "section_demo_*.txt")
    	if err != nil {
    		fmt.Printf("failed to create temp file: %v\n", err)
    		return
    	}
    	defer os.Remove(file.Name())
    	defer file.Close()
    
    	content := []byte("CHUNK1_AA_CHUNK2_BB_CHUNK3_CC_CHUNK4_DD_")
    	if _, err := file.Write(content); err != nil {
    		fmt.Printf("failed to write file: %v\n", err)
    		return
    	}
    
    	chunkSize := int64(10)
    
    	var wg sync.WaitGroup
    
    	for i := 0; i < 4; i++ {
    		wg.Add(1)
    
    		offset := int64(i) * chunkSize
    		sec := io.NewSectionReader(file, offset, chunkSize)
    
    		go func(workerID int, chunkOffset int64, sr *io.SectionReader) {
    			defer wg.Done()
    
    			data, err := io.ReadAll(sr)
    			if err != nil {
    				fmt.Printf("worker %d failed: %v\n", workerID, err)
    				return
    			}
    
    			fmt.Printf(
    				"Worker %d [offset %2d]: %s\n",
    				workerID,
    				chunkOffset,
    				data,
    			)
    		}(i, offset, sec)
    	}
    
    	wg.Wait()
    }

    Output order is intentionally nondeterministic because the workers run concurrently:

    Worker 2 [offset 20]: CHUNK3_CC_
    Worker 0 [offset  0]: CHUNK1_AA_
    Worker 3 [offset 30]: CHUNK4_DD_
    Worker 1 [offset 10]: CHUNK2_BB_

    The important guarantee is not output ordering. It is that each worker reads only its assigned range and does not modify the file's shared sequential offset.

    Concurrency boundary

    io.SectionReader does not make an arbitrary io.ReaderAt concurrently safe.

    The concurrency property comes from the combination of:

    • independent SectionReader state, and
    • a ReaderAt implementation that supports concurrent ReadAt calls.

    *os.File supports concurrent method calls, including ReadAt, so it is a natural production use case.

    Seek Boundary & Read Behavior

    Seek changes the sequential cursor within the section's logical coordinate space.

    The section's read boundary does not move when the cursor moves.

    A cursor may be positioned beyond the section size:

    Go

    sec := io.NewSectionReader(src, 10, 5)
    
    pos, err := sec.Seek(100, io.SeekStart)
    fmt.Println(pos, err)

    Output:

    100 <nil>

    The cursor is now at logical position 100, but the section still contains only 5 readable bytes.

    A subsequent Read therefore returns:

    buf := make([]byte, 10)
    
    n, err := sec.Read(buf)
    fmt.Println(n, err)

    Output:

    0 EOF

    Seeking does not expand the section.

    Negative Seek Positions

    A seek that would produce a negative logical position fails.

    For example:

    sec := io.NewSectionReader(src, 10, 5)
    
    pos, err := sec.Seek(-1, io.SeekStart)
    fmt.Println(pos, err)

    The resulting position is invalid because a SectionReader cannot expose a negative logical position.

    ReadAt Boundary Truncation

    ReadAt uses a relative offset and does not modify the sequential cursor.

    If the requested range extends beyond the section, SectionReader limits the underlying read to the section boundary.

    For example:

    Section: [A B C D E]
              0 1 2 3 4
    
    ReadAt(buf, 3), len(buf) = 4
    
    Requested logical range:
              [D E ? ?]
               └── section ends here
    
    Actual underlying read:
              [D E]
    
    Result:
              n   = 2
              err = io.EOF

    The EOF indicates that the requested logical range extended beyond the section, even if the underlying ReaderAt itself had more data available.

    io.SectionReader vs io.LimitReader

    Both can restrict the amount of data exposed to a caller, but they solve different problems.

    Propertyio.LimitReaderio.SectionReader
    Inputio.Readerio.ReaderAt
    Sequential ReadYesYes
    SeekNoYes
    ReadAtNoYes
    Fixed starting offsetNoYes
    Bounded rangeMaximum number of sequential bytesFixed source byte range
    Sequential cursorFollows underlying readerIndependent section cursor
    ReadAt changes sequential cursorN/ANo
    Same instance concurrent useGenerally noNo
    Multiple wrappers over sourceDepends on sourceSafe when underlying ReaderAt supports concurrent ReadAt
    Typical useNetwork/HTTP streamsFiles, binary containers, random-access data

    A useful rule:

    Use io.LimitReader when you already have a sequential stream and only need to impose a maximum length.

    Use io.SectionReader when you have random-access data and need a bounded, seekable view of a specific byte range.

    Conceptual Implementation

    The following is a simplified conceptual model. It focuses on the relationship between the section-relative cursor and the underlying absolute offset; implementation details such as overflow handling and optimized WriteTo paths are omitted.

    Go

    type SectionReader struct {
    	r     ReaderAt
    	base  int64
    	off   int64 // Relative sequential cursor
    	limit int64
    	n     int64 // Original section length
    }
    
    func (s *SectionReader) Read(p []byte) (n int, err error) {
    	if s.off >= s.n {
    		return 0, io.EOF
    	}
    
    	max := s.n - s.off
    	if int64(len(p)) > max {
    		p = p[:max]
    	}
    
    	n, err = s.r.ReadAt(p, s.base+s.off)
    	s.off += int64(n)
    
    	return n, err
    }
    
    func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
    	if off < 0 {
    		return 0, errors.New("negative offset")
    	}
    
    	if off >= s.n {
    		return 0, io.EOF
    	}
    
    	max := s.n - off
    	if int64(len(p)) > max {
    		p = p[:max]
    		err = io.EOF
    	}
    
    	n, readErr := s.r.ReadAt(p, s.base+off)
    	if readErr != nil {
    		return n, readErr
    	}
    
    	return n, err
    }
    
    func (s *SectionReader) Outer() (ReaderAt, int64, int64) {
    	return s.r, s.base, s.n
    }

    The important transformation is:

    section-relative offset
            +
    section base offset
    
    underlying absolute offset

    For example:

    base = 100
    ReadAt(..., 20)
    
    
    
    underlying.ReadAt(..., 120)

    This is the fundamental mechanism that allows the wrapper to expose an isolated logical address space over an existing random-access source.

    WriteTo and io.Copy

    Modern Go versions also provide an optimized WriteTo path for SectionReader.

    This matters because io.Copy checks for specialized interfaces before falling back to a generic read/write loop.

    Therefore, code such as:

    _, err := io.Copy(dst, sec)

    can use the SectionReader's specialized write path rather than necessarily reducing the operation to repeated generic Read calls.

    The important engineering point is:

    io.SectionReader is not merely a boundary-checking wrapper. It participates in Go's standard I/O fast-path interfaces.

    When performance matters, preserving the wrapper rather than immediately converting the section into an in-memory []byte can retain streaming behavior and avoid an unnecessary full copy.

    Critical Production Pitfalls

    1. Sharing One SectionReader Instance

    A SectionReader contains mutable sequential state.

    Do not use the same instance concurrently:

    sec := io.NewSectionReader(file, 0, 1024)
    
    go io.ReadAll(sec)
    go io.ReadAll(sec) // Wrong: both goroutines mutate the same cursor.

    Instead, create independent views:

    secA := io.NewSectionReader(file, 0, 1024)
    secB := io.NewSectionReader(file, 1024, 1024)

    Each wrapper owns its own cursor while both can use the same ReaderAt.

    2. Confusing ReadAt with Read

    ReadAt does not advance the sequential cursor.

    This distinction matters when code mixes random-access inspection with sequential parsing:

    header := make([]byte, 4)
    
    _, err := sec.ReadAt(header, 0)
    if err != nil {
    	return err
    }
    
    // The sequential cursor is still at its previous position.

    A ReadAt call cannot be used as a substitute for Read when the caller expects the stream position to advance.

    3. Underlying Resource Lifetime

    SectionReader does not own the underlying resource.

    For a file:

    file, err := os.Open("data.bin")
    if err != nil {
    	return err
    }
    defer file.Close()
    
    sec := io.NewSectionReader(file, 0, 100)
    
    // sec depends on file remaining open.

    Closing the file invalidates subsequent operations through the SectionReader.

    file.Close()
    
    buf := make([]byte, 10)
    _, err = sec.Read(buf) // Fails because the underlying file is closed.

    The lifetime relationship is therefore:

    SectionReader
    
         └── references ──> ReaderAt
    
                               └── resource lifetime is external

    Creating a SectionReader does not acquire ownership of that resource.

    4. Section Bounds Are Not Source Bounds

    A section limits what the SectionReader can expose. It does not change the underlying source.

    For example:

    sec := io.NewSectionReader(file, 1000, 500)

    means:

    SectionReader-visible range:
    [1000, 1500)
    
    Underlying file:
    [0, file-size)

    The original file remains fully accessible through file or another ReaderAt.

    This makes SectionReader a view, not a capability boundary or security sandbox.

    Decision Matrix

    Use the following matrix to evaluate whether io.SectionReader is the correct tool:

    SituationShould I use io.SectionReader?
    Bounded sub-range reading with Seek support from *os.File✅ Yes
    Multiple independent chunk-processing goroutines over one file✅ Yes
    Parsing bounded sections of binary containers✅ Yes
    Random-access relative reads within a fixed window✅ Yes
    Streaming data from net.Conn❌ No — use io.LimitReader
    Bounding an http.Response.Body❌ No — use io.LimitReader
    Writing to a file section❌ No — SectionReader is read-only
    Managing underlying file cleanup❌ No — lifecycle remains external
    Sharing one SectionReader between concurrent workers❌ No — create one instance per worker
    Need to expose only a fixed byte range of a random-access source✅ Yes