go / readwrap

I use small wrapper types to add functionality to io.Reader without changing the underlying implementation.

ReadCounter

Count bytes as they're read, useful for progress reporting or metrics:

type ReadCounter struct {
	r io.Reader
	n int64
}

func NewReadCounter(r io.Reader) *ReadCounter {
	return &ReadCounter{r: r}
}

func (r *ReadCounter) Read(p []byte) (int, error) {
	n, err := r.r.Read(p)
	r.n += int64(n)
	return n, err
}

func (r *ReadCounter) N() int64 { return r.n }

Usage:

rc := NewReadCounter(resp.Body)
io.Copy(dst, rc)
fmt.Printf("downloaded %d bytes\n", rc.N())

ReadCloser combiner

Attach a separate closer to a reader. Useful when chaining decompressors or transforms where the final reader doesn't close the underlying source:

type ReadCloser struct {
	r io.Reader
	c io.Closer
}

func NewReadCloser(r io.Reader, c io.Closer) *ReadCloser {
	return &ReadCloser{r: r, c: c}
}

func (r *ReadCloser) Read(p []byte) (int, error) {
	return r.r.Read(p)
}

func (r *ReadCloser) Close() error {
	if rc, ok := r.r.(io.Closer); ok {
		if err := rc.Close(); err != nil {
			r.c.Close()
			return err
		}
	}
	return r.c.Close()
}

Usage with a decompressor:

func openGzip(path string) (io.ReadCloser, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    gr, err := gzip.NewReader(f)
    if err != nil {
        f.Close()
        return nil, err
    }
    // Closing returns closes both gzip reader and file
    return NewReadCloser(gr, f), nil
}

LimitReadCloser

Like io.LimitReader but implements io.Closer:

type LimitedReadCloser struct {
	R io.ReadCloser
	N int64
}

func NewLimitedReadCloser(r io.ReadCloser, n int64) io.ReadCloser {
	return &LimitedReadCloser{R: r, N: n}
}

func (l *LimitedReadCloser) Read(p []byte) (int, error) {
	if l.N <= 0 {
		return 0, io.EOF
	}
	if int64(len(p)) > l.N {
		p = p[:l.N]
	}
	n, err := l.R.Read(p)
	l.N -= int64(n)
	return n, err
}

func (l *LimitedReadCloser) Close() error {
	return l.R.Close()
}

Usage when you need to limit bytes but also close the source:

resp, _ := http.Get(url)
// Read at most 1MB, then close connection
limited := NewLimitedReadCloser(resp.Body, 1<<20)
defer limited.Close()
data, _ := io.ReadAll(limited)

When to use

Composition

These wrappers compose naturally:

// Count bytes read from a limited, gzipped response
resp, _ := http.Get(url)
limited := NewLimitedReadCloser(resp.Body, 10<<20) // 10MB max
gr, _ := gzip.NewReader(limited)
rc := NewReadCloser(gr, limited)
counter := NewReadCounter(rc)
defer rc.Close()

io.Copy(dst, counter)
fmt.Printf("read %d compressed bytes\n", counter.N())

See errwriter for the write-side equivalent pattern.

← All articles