go 110 lines · 3 tabs

Stream and Transform a Large Log File Line-by-Line with bufio.Scanner

Shared by codesnips Sep 2026
3 tabs
package pipeline

import (
	"bufio"
	"errors"
	"io"
)

var ErrSkipLine = errors.New("skip line")

type LineFunc func(line []byte, out *bufio.Writer) error

const maxLine = 1 << 20 // 1MiB

func Transform(src io.Reader, dst io.Writer, fn LineFunc) error {
	r := bufio.NewReaderSize(src, 64*1024)
	w := bufio.NewWriterSize(dst, 64*1024)

	scanner := bufio.NewScanner(r)
	scanner.Buffer(make([]byte, 0, 64*1024), maxLine)

	for scanner.Scan() {
		err := fn(scanner.Bytes(), w)
		if errors.Is(err, ErrSkipLine) {
			continue
		}
		if err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}
	return w.Flush()
}
3 files · go Explain with highlit

This snippet shows how to process an arbitrarily large file without loading it into memory, by streaming it line-by-line through bufio.Scanner and pushing each transformed record straight to an output writer. The core idea is that a file of any size can be handled in constant memory as long as the program only ever holds one line (plus its scan buffer) at a time, rather than an entire slice of lines.

In transform.go, the Transform function wires a bufio.Reader to a bufio.Writer and drives a bufio.Scanner over the input. A key detail is scanner.Buffer(...): the default Scanner caps a token at 64KB and returns bufio.ErrTooLong on longer lines, which is a common surprise when parsing verbose log lines. Setting an explicit buffer and a larger maxLine raises that ceiling deliberately. Each line is passed to a pluggable LineFunc; returning ErrSkipLine lets a transform drop a record without aborting the whole stream, while any other error stops processing immediately. After the loop, scanner.Err() is checked separately because Scan() returning false means either clean EOF or a real error, and the two must be distinguished. The final w.Flush() is essential — a buffered writer holds bytes that would otherwise be silently lost.

In parser.go, NewApacheParser returns a concrete LineFunc that parses common-log-format access lines and re-emits only slow 5xx requests as compact JSON. It shows the realistic shape of a transform: guard clauses that return ErrSkipLine for blank or malformed lines, cheap field extraction with strings.Fields, and a per-line allocation kept small. Because the transform is just a function, it stays trivially unit-testable in isolation from the I/O plumbing.

In main.go, the command opens the input file (or reads os.Stdin), wires stdout as the sink, and applies the parser. Deferring f.Close() and surfacing the error from Transform gives a clean CLI. The trade-off of this design is that it is inherently sequential; if the transform were CPU-heavy, a worker pool fed by the scanner would parallelize it, but for I/O-bound log rewriting this single-goroutine pipeline is simpler and already saturates the disk. Reach for this pattern whenever data is line-oriented and too large to fit comfortably in RAM.


Related snips

Share this code

Here's the card — post it anywhere.

Stream and Transform a Large Log File Line-by-Line with bufio.Scanner — share card
Link copied