A common mistake is designing a function around filenames or *os.File values when it only needs to consume bytes. io.Reader defines a smaller boundary: a value supplies Read(p []byte) (n int, err error), filling the caller-provided slice. Go types satisfy this interface implicitly, without an implements declaration. section 2.5.1
Use the abstraction when the operation should remain independent of its source:
- Accept io.Reader rather than a filename when callers may provide files, strings, bytes, or other data sources. section 2.5.1
- Pair io.Reader with io.Writer to copy data without coupling the operation to a particular source or destination type. section 2.5.1
- Test reader-consuming code with strings.Reader instead of managing files; use bytes.Buffer as a convenient destination when the operation also accepts io.Writer. section 2.5.1 section 6.7
A reader-based copy test
Define copySourceToDest(source io.Reader, dest io.Writer) error. In a test, create source := strings.NewReader("foo") and dest := bytes.NewBuffer(make([]byte, 0)); call the function, then compare dest.String() with the input. The test checks the behavior without opening a file. section 2.5.1
The method signature also reflects an allocation-conscious API choice. The caller supplies the byte slice, while Read reports the number of bytes read and an error; returning a newly allocated slice would automatically make that slice escape to the heap. section 12.6.1
type Reader interface {
Read(p []byte) (n int, err error)
}
The compact interface declaration makes the contract explicit: callers provide storage, and implementations report progress and failure. section 2.5.1


