Day 25 — Embedding & Composition
Learning Objectives
- Build transparent wrappers using struct embedding
- Understand method promotion and shadowing
- Create
MeteredConnfor byte-counting — used in your TCP proxy
Key Concepts
// Embedding vs field
type Animal struct { Name string }
func (a Animal) Speak() string { return "..." }
type Dog struct {
Animal // embedding — Dog.Speak() works, Dog.Name works
Breed string
}
type Cat struct {
pet Animal // field — must use cat.pet.Speak(), cat.pet.Name
}
// Shadowing — Dog's own method takes precedence
func (d Dog) Speak() string { return "Woof" }
// Interface embedding
type ReadWriter interface {
io.Reader
io.Writer
}
Exercise: MeteredConn — Transparent Byte Counter
package main
import (
"fmt"
"net"
"sync/atomic"
)
// MeteredConn wraps net.Conn and counts bytes.
// Transparent: all net.Conn methods still work without modification.
type MeteredConn struct {
net.Conn // embedding: all net.Conn methods promoted
bytesRead atomic.Int64
bytesWritten atomic.Int64
}
func NewMeteredConn(conn net.Conn) *MeteredConn {
return &MeteredConn{Conn: conn}
}
// Override Read to count bytes
func (mc *MeteredConn) Read(b []byte) (int, error) {
n, err := mc.Conn.Read(b)
mc.bytesRead.Add(int64(n))
return n, err
}
// Override Write to count bytes
func (mc *MeteredConn) Write(b []byte) (int, error) {
n, err := mc.Conn.Write(b)
mc.bytesWritten.Add(int64(n))
return n, err
}
func (mc *MeteredConn) Stats() (read, written int64) {
return mc.bytesRead.Load(), mc.bytesWritten.Load()
}
// TimedHTTPHandler wraps http.Handler and records latency
type TimedHTTPHandler struct {
http.Handler
histogram *RingBuffer[int64]
}
func NewTimedHTTPHandler(h http.Handler, histSize int) *TimedHTTPHandler {
return &TimedHTTPHandler{Handler: h, histogram: NewRingBuffer[int64](histSize)}
}
func (th *TimedHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
th.Handler.ServeHTTP(w, r)
th.histogram.Add(time.Since(start).Milliseconds())
}
func (th *TimedHTTPHandler) P99() int64 {
samples := th.histogram.All()
if len(samples) == 0 { return 0 }
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
idx := int(float64(len(samples)) * 0.99)
if idx >= len(samples) { idx = len(samples) - 1 }
return samples[idx]
}
func main() {
// Create a TCP connection pair for testing
server, client := net.Pipe()
defer server.Close()
defer client.Close()
// Wrap client connection with metering
metered := NewMeteredConn(client)
// Write some data
go func() {
metered.Write([]byte("GET /api/data HTTP/1.1\r\nHost: example.com\r\n\r\n"))
}()
// Read from server side
buf := make([]byte, 1024)
n, _ := server.Read(buf)
server.Write(buf[:n]) // echo back
// Read response
metered.Read(buf)
read, written := metered.Stats()
fmt.Printf("Bytes read: %d, written: %d\n", read, written)
}
Real-World Context
MeteredConn is used in your TCP proxy (Week 5) to count bytes per connection and expose them via expvar. This is how your load balancer tracks backend bandwidth usage — billing, capacity planning, and anomaly detection (a connection suddenly transferring 10x normal volume).