zoobzio December 31, 2023 Edit this page

Core Concepts

Clock Selection

clockz provides two clock implementations. Choosing between them is straightforward.

RealClock

Use RealClock when:

  • Running in production
  • Integrating with external systems that expect real time
  • Measuring actual wall-clock performance
  • Any code path that faces the outside world
service := NewService(clockz.RealClock)

RealClock is a package-level variable—there's no constructor. It delegates directly to the time package with zero overhead.

FakeClock

Use FakeClock when:

  • Writing unit tests
  • Testing timeout behavior
  • Simulating time-dependent scenarios
  • Eliminating test flakiness from timing
// Start at Unix epoch
clock := clockz.NewFakeClock()

// Start at specific time
clock := clockz.NewFakeClockAt(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))

Time in FakeClock never advances automatically. You control it explicitly.

Dependency Injection Pattern

The core pattern is simple: accept a Clock interface instead of calling time functions directly.

Before (Hard to Test)

type Cache struct {
    data    map[string]entry
}

type entry struct {
    value   interface{}
    expires time.Time
}

func (c *Cache) Get(key string) (interface{}, bool) {
    e, ok := c.data[key]
    if !ok || time.Now().After(e.expires) {  // Direct time dependency
        return nil, false
    }
    return e.value, true
}

Testing this requires either:

  • Waiting for real time to pass (slow)
  • Complex mocking of the time package (fragile)
  • Accepting flaky tests (unreliable)

After (Testable)

type Cache struct {
    clock clockz.Clock
    data  map[string]entry
}

func NewCache(clock clockz.Clock) *Cache {
    return &Cache{
        clock: clock,
        data:  make(map[string]entry),
    }
}

func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.data[key] = entry{
        value:   value,
        expires: c.clock.Now().Add(ttl),
    }
}

func (c *Cache) Get(key string) (interface{}, bool) {
    e, ok := c.data[key]
    if !ok || c.clock.Now().After(e.expires) {  // Injected clock
        return nil, false
    }
    return e.value, true
}

Now testing is trivial:

func TestCacheExpiration(t *testing.T) {
    clock := clockz.NewFakeClockAt(time.Now())
    cache := NewCache(clock)

    cache.Set("key", "value", 5*time.Minute)

    // Before expiration
    if _, ok := cache.Get("key"); !ok {
        t.Fatal("expected value before expiration")
    }

    // After expiration
    clock.Advance(6 * time.Minute)

    if _, ok := cache.Get("key"); ok {
        t.Fatal("expected no value after expiration")
    }
}

Timers and Tickers

Timers

Timers fire once after a duration:

timer := clock.NewTimer(5 * time.Second)
defer timer.Stop()

select {
case <-timer.C():
    fmt.Println("Timer fired")
case <-ctx.Done():
    fmt.Println("Cancelled")
}

Key timer operations:

  • Stop() — Prevents the timer from firing, returns true if it was active
  • Reset(d) — Restarts the timer for duration d, returns true if it was active
  • C() — Returns the channel that receives the firing time

Tickers

Tickers fire repeatedly at intervals:

ticker := clock.NewTicker(time.Second)
defer ticker.Stop()  // Always stop tickers

for i := 0; i < 5; i++ {
    <-ticker.C()
    fmt.Printf("Tick %d\n", i+1)
}

Important: Always call Stop() on tickers to release resources.

Context Integration

clockz provides clock-aware context timeouts and deadlines:

// Timeout after duration
ctx, cancel := clock.WithTimeout(ctx, 30*time.Second)
defer cancel()

// Deadline at specific time
deadline := clock.Now().Add(1 * time.Hour)
ctx, cancel := clock.WithDeadline(ctx, deadline)
defer cancel()

With FakeClock, contexts cancel when fake time reaches the deadline—no real waiting required.

func TestTimeout(t *testing.T) {
    clock := clockz.NewFakeClockAt(time.Now())

    ctx, cancel := clock.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Context is not cancelled yet
    if ctx.Err() != nil {
        t.Fatal("context cancelled too early")
    }

    // Advance past deadline
    clock.Advance(6 * time.Second)

    // Now it's cancelled
    if ctx.Err() != context.DeadlineExceeded {
        t.Fatalf("expected DeadlineExceeded, got %v", ctx.Err())
    }
}

Thread Safety

Both RealClock and FakeClock are fully thread-safe:

  • Multiple goroutines can call any clock method concurrently
  • FakeClock.Advance() and SetTime() are safe to call while other goroutines wait on timers
  • Timer and ticker operations are safe for concurrent use

This means your production code and tests can use clocks freely in concurrent scenarios without additional synchronization.