• English
  • Example: io.ReadSeeker / io.WriteSeeker

    io.ReadSeeker and io.WriteSeeker combine sequential stream I/O with position control via Seek.

    They are useful when an operation needs to jump to another location and then continue reading or writing from there.

    Typical usage pattern:

    seek → read/write → seek → read/write

    If every access already carries an explicit offset, io.ReaderAt or io.WriterAt is usually a better fit because they do not alter the stream's current seek position.

    Quick Example

    Read from a specific position:

    package main
    
    import (
    	"fmt"
    	"io"
    	"log"
    	"strings"
    )
    
    func main() {
    	r := strings.NewReader("0123456789")
    
    	if _, err := r.Seek(5, io.SeekStart); err != nil {
    		log.Fatal(err)
    	}
    
    	buf := make([]byte, 3)
    	if _, err := io.ReadFull(r, buf); err != nil {
    		log.Fatal(err)
    	}
    
    	fmt.Println(string(buf)) // 567
    }

    Seek changes the reader's current position. Subsequent reads start at this new position.

    Common Use-Case: ReadSeeker

    Use io.ReadSeeker when a function needs to inspect different sections of the same stream, for example, by re-reading a header:

    func readHeader(r io.ReadSeeker) ([]byte, error) {
    	if _, err := r.Seek(0, io.SeekStart); err != nil {
    		return nil, err
    	}
    
    	header := make([]byte, 16)
    	if _, err := io.ReadFull(r, header); err != nil {
    		return nil, err
    	}
    
    	return header, nil
    }

    Common Use-Case: WriteSeeker

    io.WriteSeeker is useful when you need to write a placeholder first, then go back and fill in a value after its final value is known, such as a length prefix:

    import "encoding/binary"
    
    func writeRecord(w io.WriteSeeker, payload []byte) error {
    	if _, err := w.Seek(0, io.SeekStart); err != nil {
    		return err
    	}
    
    	// Write a placeholder for the length.
    	if _, err := w.Write(make([]byte, 4)); err != nil {
    		return err
    	}
    
    	if _, err := w.Write(payload); err != nil {
    		return err
    	}
    
    	// Jump back to overwrite the placeholder.
    	if _, err := w.Seek(0, io.SeekStart); err != nil {
    		return err
    	}
    
    	var lenBuf [4]byte
    	binary.BigEndian.PutUint32(lenBuf[:], uint32(len(payload)))
    
    	_, err := w.Write(lenBuf[:])
    	return err
    }

    Common Mistakes

    1. Assuming Read always fills the buffer

    Read may return fewer bytes than requested without an error.

    Use io.ReadFull when you need exactly the requested number of bytes:

    _, err := io.ReadFull(r, buf)
    if err != nil {
    	return err
    }

    2. Using Seek for independent random-access reads

    Seek changes the stream's current position.

    If you simply want bytes at a known offset, prefer ReadAt:

    n, err := r.ReadAt(buf, 1024)

    ReadAt uses an explicit offset and does not modify the current seek position.

    3. Sharing one seeker between independent or concurrent operations

    ReadSeeker and WriteSeeker carry mutable cursor state. One caller's Seek can move the cursor while another caller expects it somewhere else.

    Sharing a seeker between concurrent operations requires synchronization.

    For independent access, use ReaderAt / WriterAt, or give each operation its own seeker.

    API Selection

    NeedPreferKey semantics
    Sequential read/write onlyio.Reader / io.WriterSequential stream access
    Move and reuse the current cursor positionio.ReadSeeker / io.WriteSeekerStateful cursor; Seek changes position
    Access data at a known fixed offsetio.ReaderAt / io.WriterAtExplicit offset; does not change seek position
    Read inside a bounded sub-regionio.SectionReaderBounded read-only view over a ReaderAt
    Write relative to a fixed base offsetio.OffsetWriterMaps logical offsets to a base position

    Rule of Thumb

    Seek moves the current position; At operates at an explicit offset.