ByteView and Sink Internals in Groupcache

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 – returns len(b) or len(s).
  • ByteSlice() []byte – returns a copy of the data as a slice.
  • String() string – returns the data as a string (no copy when b == nil).
  • At(i int) byte – constant-time index access.
  • Slice(from, to int) ByteView and SliceFrom(from int) ByteView – produce a new ByteView sharing the same backing store.
  • Copy(dst []byte) int – copies bytes in to dst and 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? Use StringSink(&s).
  • Already have a []byte buffer? Use AllocatingByteSliceSink(&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.

Tags: groupcache ByteView Sink Protobuf Go

Posted on Thu, 24 Sep 2026 16:54:46 +0000 by waynewex