Day 6 — Interfaces
Learning Objectives
- Define behavior with interfaces, not data
- Write code against interfaces to enable testing (mock the
Storageinterface in tests) - Understand why Go interfaces are implicit (no
implementskeyword)
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.