Simple buffered line reader for Go: read a stream line by line — or by raw
bytes, or as a plain io.Reader — over one chain of reusable blocks, without
ever losing a byte between styles.
lr := linereader.NewLineReader(file, 0, 0) // 0 = defaults (4096)
for {
line, err := lr.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return err
}
fmt.Printf("%s (ended by %#x)\n", line, int(lr.LastEolType))
}ReadLine() ([]byte, error)— the next line, terminator consumed and never included. A line ends at whatever happens first:\r,\nor EOF. The pair\r\ncounts as one terminator — even when it rides a block boundary (a\ras the last buffered byte makes the reader pull another block before deciding; unless EOF got there first: a lone CR then). What ended the line lands inLastEolType:EolLf,EolCr,EolCrLf, orEolEoffor a final unterminated line.ReadBytes(n uint64) ([]byte, error)— the next n raw bytes, buffer first: whatever already sits in the chain is served before touching the stream. Short only at the end.PeekBytes(n uint64) ([]byte, error)— look without taking: the same buffer-first pull, but the offset stays put — the next read serves these same bytes again.SkipBytes(n uint64) (uint64, error)— exactly ReadBytes, only returning how many bytes were skipped instead of a buffer (literally: that is the implementation). Peek and skip together make the classic sniff: peek a BOM (or any magic bytes), decide, skip it.Read(p []byte) (int, error)— the canonicalio.Readercontract: read the header of something withReadLine, then hand the reader itself to anyio.Readerconsumer — it continues right where you stopped, buffered bytes first.IoLineReader— the interface for line consumers:interface { ReadLine() ([]byte, error) }. Sign your functions against it and any line source can stand in;LineReaderis the reference implementation.
Blocks are grabbed from the stream (BlockSize, default 4096) and chained;
lines are found across the chain. Consumed blocks are discarded whole —
bytes never move. A line contained in one block is served as a sub-slice,
no copy; one crossing blocks gets assembled. MaxLineLength (default 4096)
caps how far a line may grow without an EOL — an error instead of unbounded
memory.
Buffered data always comes out first: errors (io.EOF included) surface only
once the chain is drained. A final unterminated line is data, not an error —
it comes back with err == nil and LastEolType == EolEof; only the next
call returns io.EOF.
The stream's error is kept in CachedError and served once, then
cleared: the next attempt asks the stream again. A source that comes back to
life after an EOF — a growing file, tail-follow style — revives with a plain
poll loop, no rearming:
for {
line, err := lr.ReadLine()
if err == io.EOF {
time.Sleep(interval) // the source may grow
continue
}
...
}go get github.com/pablo-botella/linereader
MIT — see LICENSE.