os, io, bufio & path/filepath
File and stream I/O in Go layers small interfaces into composable pipelines.
Search across all documentation pages
File and stream I/O in Go layers small interfaces into composable pipelines.
os talks to the filesystem, io defines movement of bytes, bufio reduces syscall overhead, and path/filepath builds portable paths.
Most Go programs read config, serve uploads, or pipe data between processes.
The stdlib models all of that as io.Reader and io.Writer implementations.
os.Open returns an *os.File you can pass to io.Copy, bufio.NewScanner, or json.NewDecoder.
Cross-platform tools must never hardcode / or \; filepath.Join picks the correct separator.
Quick-reference recipe card - copy-paste ready.
path := filepath.Join("data", "input.txt")
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
var out bytes.Buffer
if _, err := io.Copy(&out, f); err != nil {
return err
}When to reach for this:
bufio.Scanner.package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func copyFiltered(srcPath, dstPath, prefix string) (int, error) {
srcPath = filepath.Clean(srcPath)
dstPath = filepath.Clean(dstPath)
src, err := os.Open(srcPath)
if err != nil {
return 0, fmt.Errorf("open src: %w", err)
}
defer src.Close()
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return 0, fmt.Errorf("mkdir: %w", err)
}
dst, err := os.Create(dstPath)
if err != nil {
return 0, fmt.Errorf("create dst: %w", err)
}
defer func() {
if cerr := dst.Close(); cerr != nil && err == nil {
err = cerr
}
}()
scanner := bufio.NewScanner(src)
writer := bufio.NewWriter(dst)
defer writer.Flush()
lines := 0
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, prefix) {
if _, err := writer.WriteString(line + "\n"); err != nil {
return lines, err
}
lines++
}
}
if err := scanner.Err(); err != nil {
return lines, err
}
return lines, nil
}
func main() {
n, err := copyFiltered("input.log", filepath.Join("out", "filtered.log"), "ERR")
if err != nil {
panic(err)
}
fmt.Println("wrote lines:", n)
}What this demonstrates:
filepath.Clean and filepath.Join for safe path assembly.os.MkdirAll before creating nested output files.bufio.Scanner for line iteration and bufio.Writer for batched writes.io.Copy reads from any Reader and writes to any Writer using an internal buffer pool.*os.File reads and writes map to OS file descriptors; offsets are tracked per handle.bufio.Reader and bufio.Writer wrap streams to batch syscalls.filepath operates on paths as strings; it does not access the filesystem (use os for that).| Package | Responsibility | Typical entry points |
|---|---|---|
os | Files, env, process, args | Open, ReadFile, WriteFile, Getenv |
io | Stream contracts and helpers | Copy, ReadAll, LimitReader, Pipe |
bufio | Buffering and scanning | NewScanner, NewReader, NewWriter |
path/filepath | OS-specific path ops | Join, Clean, Walk, Rel |
// Prefer io.Copy over manual Read loops for piping.
// Limit uploads to prevent memory exhaustion:
limited := io.LimitReader(r.Body, 1<<20) // 1 MiB cap
// Walk directory trees portably:
filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
// process file
return nil
})defer f.Close() and propagate close errors on writers.path instead of path/filepath - path is for URL-like forward-slash paths, not Windows filesystem paths. Fix: import path/filepath for OS paths.Scanner.Err() - Loop ends on I/O errors silently. Fix: check sc.Err() after for sc.Scan().sc.Buffer(make([]byte, 0, 64*1024), maxToken).Read fills the buffer - Read may return n < len(buf) with nil error. Fix: loop until io.EOF or use io.ReadFull.os.Stat then os.Open - File may change between checks. Fix: open first, then validate content or use file locks when required.| Alternative | Use When | Don't Use When |
|---|---|---|
os.ReadFile / os.WriteFile | Small, bounded files | Streaming large uploads or logs |
mmap via syscall or third-party | Zero-copy read of huge files | Portability and simplicity matter more |
embed.FS | Ship static assets in binary | User-provided paths on disk |
afero or io/fs abstractions | Test filesystem without disk | Simple CLI tools with real files only |
io.Reader is an interface with one Read method.
*os.File implements io.Reader (and io.Writer) for filesystem-backed streams.
Functions accepting io.Reader work with files, network bodies, strings, and buffers alike.
Use bufio when you perform many small reads or writes and want fewer syscalls.
Use direct reads when you already have large buffers or memory-mapped data.
Join inserts the correct separator for the OS and cleans redundant elements.
Manual concatenation breaks on Windows and invites double-slash bugs.
Clean simplifies . and .. segments but does not validate trust.
Validate user input roots separately before opening files.
data, err := os.ReadFile(path) then string(data).
For large files, stream with bufio.Scanner or io.Copy instead.
Use the minimum needed; 0o600 for private config, 0o644 for world-readable artifacts.
Never default to 0777 in application code.
Yes.
Call sc.Split with a custom SplitFunc or use bufio.NewReader with ReadString.
Read once into a buffer, or use io.TeeReader to copy bytes to a secondary writer while reading.
There is no built-in fan-out Reader.
Close errors are ignored unless you assign them in a deferred closure.
Writers especially need close error checks to flush buffers.
When accepting untrusted input streams (HTTP uploads, RPC bodies) to cap memory and CPU usage.
Pair with max size checks before io.ReadAll.
Yes for scratch dirs you created.
Never point RemoveAll at user-supplied paths without validating they stay under an expected root.
filepath.WalkDir passes os.DirEntry without extra Stat calls, reducing syscalls on large trees.
Prefer WalkDir in new code.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026