ByteView: an immutable view over bytes or strings
The ByteView type is a tiny, zero-allocation wrapper that can hold either a []byte or a string. Internally it is nothing more than
type ByteView struct {
b []byte
s string
}
Exactly one of the two fields is non-nil/-empty; the other must be zero. All exported methods respect this invariant and never mutate the underlying data, so ByteView is safe to share concurrently.
Core helpers
Len() int– returnslen(b)orlen(s).ByteSlice() []byte– returns a copy of the data as a slice.String() string– returns the data as a string (no copy whenb == nil).At(i int) byte– constant-time index access.Slice(from, to int) ByteViewandSliceFrom(from int) ByteView– produce a newByteViewsharing the same backing store.Copy(dst []byte) int– copies bytes in todstand returns the count.
Comparison helpers
func (v ByteView) Equal(other ByteView) bool {
switch {
case other.b != nil:
return v.equalBytes(other.b)
default:
return v.equalString(other.s)
}
}
Both equalBytes and equalString compare lengths first, then every byte/rune, avoiding allocations.
IO adapters
func (v ByteView) Reader() io.ReadSeeker {
if v.b != nil {
return bytes.NewReader(v.b)
}
return strings.NewReader(v.s)
}
func (v ByteView) ReadAt(p []byte, off int64) (int, error) { ... }
func (v ByteView) WriteTo(w io.Writer) (int64, error) { ... }
These helpers let ByteView act as a drop-in replacement for any io.Reader, io.ReaderAt, or io.WriterTo source.
Sink: a destination abstraction
The Sink interface is used by groupcache to deliver a value to the caller without prescribing how that value should be stored.
type Sink interface {
SetString(string) error
SetBytes([]byte) error
SetProto(proto.Message) error
view() (ByteView, error)
}
Five concrete implementations exist, each tailored to a different use-case.
stringSink
type stringSink struct{ sp *string }
func StringSink(sp *string) Sink { return &stringSink{sp} }
func (s *stringSink) SetString(v string) error { *s.sp = v; return nil }
func (s *stringSink) SetBytes(b []byte) error { *s.sp = string(b); return nil }
func (s *stringSink) SetProto(m proto.Message) error {
b, err := proto.Marshal(m)
if err != nil { return err }
*s.sp = string(b)
return nil
}
func (s *stringSink) view() (ByteView, error) { return ByteView{s: *s.sp}, nil }
byteViewSink
type byteViewSink struct{ dst *ByteView }
func (s *byteViewSink) setView(v ByteView) error { *s.dst = v; return nil }
func (s *byteViewSink) SetBytes(b []byte) error {
*s.dst = ByteView{b: cloneBytes(b)}
return nil
}
func (s *byteViewSink) SetString(v string) error {
*s.dst = ByteView{s: v}
return nil
}
func (s *byteViewSink) SetProto(m proto.Message) error {
b, err := proto.Marshal(m)
if err != nil { return err }
*s.dst = ByteView{b: b}
return nil
}
func (s *byteViewSink) view() (ByteView, error) { return *s.dst, nil }
protoSink
Stores the protobuf message itself (dst) plus its serialized form (v ByteView).
type protoSink struct {
dst proto.Message
v ByteView
}
func (s *protoSink) SetBytes(b []byte) error {
if err := proto.Unmarshal(b, s.dst); err != nil { return err }
s.v = ByteView{b: cloneBytes(b)}
return nil
}
func (s *protoSink) view() (ByteView, error) { return s.v, nil }
allocBytesSink
Alllocates an exact-sized slice and hands ownership to the caller.
type allocBytesSink struct {
dst *[]byte
v ByteView
}
func (s *allocBytesSink) setBytesOwned(b []byte) error {
if s.dst == nil { return errors.New("nil AllocatingByteSliceSink *[]byte dst") }
*s.dst = cloneBytes(b)
s.v = ByteView{b: b}
return nil
}
truncBytesSink
Like allocBytesSink, but silently truncates or shrinks the destination slice to the actual number of bytes received.
func (s *truncBytesSink) setBytesOwned(b []byte) error {
if s.dst == nil { return errors.New("nil TruncatingByteSliceSink *[]byte dst") }
n := copy(*s.dst, b)
*s.dst = (*s.dst)[:n] // shrink
s.v = ByteView{b: b}
return nil
}
Utility helpers
func cloneBytes(b []byte) []byte {
c := make([]byte, len(b))
copy(c, b)
return c
}
The helper guarantees that the returned slice is independent of the argument, preventing accidental mutation of cached data.
Putting it together
When groupcache fetches or computes a value, it uses a Sink to decide where the bytes should land. Callers pick whichever concrete sink matches their needs:
- Need a
string? UseStringSink(&s). - Already have a
[]bytebuffer? UseAllocatingByteSliceSink(&buf). - Want a protobuf message? Use
ProtoSink(msg).
Internally, every path ends by constructing a ByteView that is cheap to copy, safe to share, and ready to serve as an io.Reader when the HTTP peer layer needs to stream the response.