Small Go helpers for line-based streaming I/O. The package has no third-party dependencies and is designed for newline-delimited streams such as logs, command output, and JSONL-like data.
go-sio uses Go modules and requires Go 1.26 or newer.
go get github.com/maxwu/go-sio@latestimport "github.com/maxwu/go-sio"
// Optional explicit alias:
// import go_sio "github.com/maxwu/go-sio"StreamReader: reads anio.Readerline by line and applies aStringLineFilter.NewJSONFilterReadCloser: wraps anio.ReadCloserand keeps only newline-delimited tokens that are valid JSON.NewTeeReaderCloser: wraps anio.ReadCloserwithio.TeeReaderwhile preservingClose.NewReadCloser: combines anio.Readerandio.Closerinto oneio.ReadCloser.
package main
import (
"fmt"
"io"
"log"
"strings"
"github.com/maxwu/go-sio"
)
func main() {
input := strings.NewReader("one\n\ntwo\nthree")
filter := func(line string) (string, error) {
if line == "\n" {
return "", nil // drop blank lines
}
return strings.ToUpper(line), nil
}
sr := go_sio.NewStreamReader(input, filter)
if sr == nil {
log.Fatal("nil reader")
}
out, err := io.ReadAll(sr)
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}package main
import (
"fmt"
"io"
"log"
"os"
"github.com/maxwu/go-sio"
)
func main() {
file, err := os.Open("stream.log")
if err != nil {
log.Fatal(err)
}
rc := go_sio.NewJSONFilterReadCloser(file)
body, readErr := io.ReadAll(rc)
closeErr := rc.Close()
if readErr != nil {
log.Fatal(readErr)
}
if closeErr != nil {
log.Fatal(closeErr)
}
fmt.Print(string(body))
}package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"github.com/maxwu/go-sio"
)
func main() {
file, err := os.Open("stream.log")
if err != nil {
log.Fatal(err)
}
var captured bytes.Buffer
rc := go_sio.NewTeeReaderCloser(file, &captured)
_, copyErr := io.Copy(os.Stdout, rc)
closeErr := rc.Close()
if copyErr != nil {
log.Fatal(copyErr)
}
if closeErr != nil {
log.Fatal(closeErr)
}
fmt.Println("Captured:", captured.String())
}type StringLineFilter func(string) (string, error): transforms one line at a time. Return""to drop the line. Return an error to stop reading.var ErrNilReader error: returned by(*StreamReader).Readon a nil receiver.var NopFilter StringLineFilter: pass-through filter used whenNewStreamReaderreceives a nil filter.func NewStreamReader(r io.Reader, f StringLineFilter) *StreamReader: returns nil whenris nil.func NewJSONFilterReadCloser(r io.ReadCloser) io.ReadCloser: filters newline-delimited tokens withencoding/json.Valid.func NewTeeReaderCloser(r io.ReadCloser, w io.Writer) *TeeReaderCloser: tees reads intowand closesr.func NewReadCloser(r io.Reader, c io.Closer) *ReadCloser: storesrandcas a combinedio.ReadCloser.
Use this section as the source of truth for humans and AI agents changing the package.
- Input is line-oriented.
StreamReaderusesbufio.Scannerwith a custom split function that keeps\nwhen present. - The filter receives each line exactly as scanned. The last line may not include
\n. - Returning
""from a filter drops that line. It does not emit an empty line. - Scanner token limits still apply. A line larger than Go's default scanner token size, about 64 KiB, returns a scanner error.
NewJSONFilterReadCloservalidates each newline-delimited token independently. It does not parse multi-line JSON records and does not reformat JSON.- Any complete JSON value accepted by
encoding/json.Validis kept, including objects, arrays, strings, numbers, booleans, and null. - Closing wrappers returned by
NewJSONFilterReadCloserorNewTeeReaderClosercloses the original reader. Callers should close only the wrapper. NewReadCloserandNewTeeReaderCloserdo not validate nil components. Passing nil readers, writers, or closers may panic later when read or closed.
Project rules:
- Use only the Go standard library.
- Keep production code at 100% unit-test coverage.
- Keep docs aligned with
go.mod, exported APIs, tests, and CI.
Run the same checks used by CI before opening a pull request:
golangci-lint run ./...
go test -count=1 -race ./... -coverprofile=coverage.out
go tool cover -func=coverage.out | grep -qE '^total:.*100\.0%$'
go test -bench . -run '^$' ./...See CONTRIBUTING.md for the contributor workflow and AGENTS.md for agent-specific constraints.
MIT. See LICENSE.