Frontend Labs
⋯

Day 6 — Interfaces

Learning Objectives

  • Define behavior with interfaces, not data
  • Write code against interfaces to enable testing (mock the Storage interface in tests)
  • Understand why Go interfaces are implicit (no implements keyword)

Key Concepts

// Interface = set of method signatures
// Any type that has these methods satisfies the interface — automatically
type Storage interface {
    Set(key, value string) error
    Get(key string) (string, bool)
    Delete(key string)
}

// io.Writer is an interface — net.Conn, os.File, bytes.Buffer all satisfy it
// This is why io.Copy works with any two types

// Type assertion — check the concrete type at runtime
var s Storage = NewMemoryStorage()
if ms, ok := s.(*MemoryStorage); ok {
    // access MemoryStorage-specific methods
    ms.Len()
}

// Type switch — dispatch on concrete type
switch v := s.(type) {
case *MemoryStorage:
    fmt.Println("memory, size:", v.Len())
case *RedisStorage:
    fmt.Println("redis, addr:", v.Addr())
}

Exercise: Storage Interface with Metrics Wrapper

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

type Storage interface {
    Set(key, value string) error
    Get(key string) (string, bool)
    Delete(key string)
}

// MemoryStorage is the real implementation
type MemoryStorage struct {
    mu   sync.RWMutex
    data map[string]string
    maxSize int
}

func NewMemoryStorage(maxSize int) *MemoryStorage {
    return &MemoryStorage{data: make(map[string]string), maxSize: maxSize}
}

func (m *MemoryStorage) Set(key, value string) error {
    m.mu.Lock()
    defer m.mu.Unlock()
    if len(m.data) >= m.maxSize && m.data[key] == "" {
        return fmt.Errorf("storage full: max %d entries", m.maxSize)
    }
    m.data[key] = value
    return nil
}

func (m *MemoryStorage) Get(key string) (string, bool) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    v, ok := m.data[key]
    return v, ok
}

func (m *MemoryStorage) Delete(key string) {
    m.mu.Lock()
    defer m.mu.Unlock()
    delete(m.data, key)
}

// MetricsStorage wraps any Storage and counts operations.
// This is the Decorator pattern — same interface, added behavior.
type MetricsStorage struct {
    inner  Storage
    gets   atomic.Int64
    sets   atomic.Int64
    hits   atomic.Int64
    misses atomic.Int64
}

func NewMetricsStorage(inner Storage) *MetricsStorage {
    return &MetricsStorage{inner: inner}
}

func (m *MetricsStorage) Set(key, value string) error {
    m.sets.Add(1)
    return m.inner.Set(key, value)
}

func (m *MetricsStorage) Get(key string) (string, bool) {
    m.gets.Add(1)
    v, ok := m.inner.Get(key)
    if ok {
        m.hits.Add(1)
    } else {
        m.misses.Add(1)
    }
    return v, ok
}

func (m *MetricsStorage) Delete(key string) { m.inner.Delete(key) }

func (m *MetricsStorage) Stats() string {
    return fmt.Sprintf("gets=%d hits=%d misses=%d sets=%d hit_rate=%.1f%%",
        m.gets.Load(), m.hits.Load(), m.misses.Load(), m.sets.Load(),
        float64(m.hits.Load())/float64(max(m.gets.Load(), 1))*100)
}

func max(a, b int64) int64 {
    if a > b { return a }
    return b
}

func main() {
    base := NewMemoryStorage(100)
    store := NewMetricsStorage(base)

    store.Set("api_key_abc123", "user:42")
    store.Set("api_key_def456", "user:99")

    if v, ok := store.Get("api_key_abc123"); ok {
        fmt.Println("auth:", v)
    }
    store.Get("api_key_unknown") // miss

    fmt.Println(store.Stats())
}

Real-World Context

Your API gateway (Week 11) will have an AuthStore interface. In production: Redis. In tests: MemoryStorage. The metrics wrapper is how you add observability without modifying the core implementation — the same pattern your team uses for database clients.


Generated from week1.md · 8 Oct 2026 16:36