Go’s io package provides a uniform, composable interface for streaming data through Reader and Writer abstractions.
io.Reader
The io.Reader interface defines a minimal contract for reading sequential data:
type Reader interface {
Read(p []byte) (n int, err error)
}
The Read method fills the provided byte slice p with up to len(p) bytes. Its behavior is subtle and critical to implement correctly:
- Attempts to read up to
len(p)bytes. nmay be less thanlen(p)(e.g., end of stream, or partial availability).- In case of error,
Readmay still return usable bytes inp(e.g., abrupt close of a TCP connection). - On final stream exhaustion,
Readmay returnn > 0anderr == io.EOF; afterward, all future calls returnn = 0,err == io.EOF. - A return of
n = 0anderr == nildoes not indicate end of stream—it means no data was available yet.
Because of this complexity, Go’s stendard library already includes robust Reader implementations.
strings.NewReader
Creates a Reader from a string for efficient, read-only access:
reader := strings.NewReader("Clear is better than clever")
buf := make([]byte, 4)
for {
n, err := reader.Read(buf)
if err != nil {
if err == io.EOF {
fmt.Println(string(buf[:n]))
break
}
log.Fatal(err)
}
fmt.Println(string(buf[:n]))
}
Custom Reader Implementations
A basic implementation of an alphaReader that filters non-alphabetic characters:
type alphaFilter struct {
source string
pos int
}
func (r *alphaFilter) Read(p []byte) (int, error) {
if r.pos >= len(r.source) {
return 0, io.EOF
}
count := 0
end := min(len(p), len(r.source)-r.pos)
for i := 0; i < end; i++ {
ch := r.source[r.pos+i]
if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') {
p[count] = ch
count++
}
}
r.pos += end
return count, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
A more idiomatic and flexible variant uses composition with an existing io.Reader, enabling reuse across readers like os.File or net.Conn:
type alphaFilter struct {
inner io.Reader
}
func (r *alphaFilter) Read(p []byte) (int, error) {
n, err := r.inner.Read(p)
if err != nil {
return n, err
}
// Filter in-place for efficiency
writeIdx := 0
for i := 0; i < n; i++ {
ch := p[i]
if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') {
p[writeIdx] = ch
writeIdx++
}
}
return writeIdx, nil
}
Usage with file input:
file, _ := os.Open("input.txt")
defer file.Close()
reader := &alphaFilter{inner: file}
buf := make([]byte, 8)
for {
n, err := reader.Read(buf)
if err == io.EOF {
break
}
fmt.Print(string(buf[:n]))
}
io.Writer
The io.Writer interface defines a contract for writing sequential data:
type Writer interface {
Write(p []byte) (n int, err error)
}
Example using bytes.Buffer to accumulate output:
var buffer bytes.Buffer
messages := []string{
"Data flows through streams.",
"Errors should be explicit.",
}
for _, msg := range messages {
n, err := buffer.Write([]byte(msg))
if err != nil || n != len(msg) {
log.Fatalf("write failed: %v", err)
}
}
fmt.Println(buffer.String())
Custom Writer with Channels
Implementing a channel-based writer for asynchronous data consumption:
type channelWriter struct {
ch chan byte
}
func (w *channelWriter) Write(p []byte) (int, error) {
for _, b := range p {
w.ch <- b
}
return len(p), nil
}
func (w *channelWriter) Close() error {
close(w.ch)
return nil
}
func (w *channelWriter) Reader() <-chan byte {
return w.ch
}
// Usage
writer := &channelWriter{ch: make(chan byte, 256)}
go func() {
defer writer.Close()
writer.Write([]byte("Go"))
writer.Write([]byte("er"))
}()
for b := range writer.Reader() {
fmt.Printf("%c", b)
}
fmt.Println()
File Streams
*os.File implements both io.Reader and io.Writer, making it ideal for file streaming:
// Write to file
file, _ := os.Create("output.txt")
defer file.Close()
data := []byte("Go streams are efficient.")
n, _ := file.Write(data)
if n != len(data) {
log.Fatal("incomplete write")
}
// Read from file
file2, _ := os.Open("output.txt")
defer file2.Close()
buf := make([]byte, 8)
for {
n, err := file2.Read(buf)
if err == io.EOF {
break
}
fmt.Print(string(buf[:n]))
}
Standard streams (os.Stdout, os.Stdin, os.Stderr) are also *os.File instances:
os.Stdout.Write([]byte("Hello from Go!\n"))
High-Level Helpers
io.Copy
Copies data from a Reader to a Writer using an internal buffer:
src := strings.NewReader("Streaming is easy.")
dst := os.Stdout
_, err := io.Copy(dst, src)
if err != nil {
log.Fatal(err)
}
io.WriteString
file, _ := os.Create("greeting.txt")
defer file.Close()
io.WriteString(file, "Hello, world!")
Pipe: In-Memory Stream Bridge
io.Pipe connects a Reader and Writer in-memory for goroutine-based pipelines:
reader, writer := io.Pipe()
go func() {
defer writer.Close()
io.WriteString(writer, "Async message")
}()
buf := make([]byte, 64)
n, _ := reader.Read(buf)
fmt.Println(string(buf[:n]))
Buffered I/O
bufio enables high-level text processing by wrapping unbuffered readers/writers:
file, _ := os.Open("lines.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
Convenience Utilities
The legacy io/ioutil package providesBatch helpers like:
contents, err := ioutil.ReadFile("data.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(contents))