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
- ReadCounter: Download progress, bandwidth metrics, transfer logging
- ReadCloser combiner: Chaining transforms (gzip, encryption, etc.)
- LimitReadCloser: Capping response sizes from untrusted sources
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.