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
timepackage (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 activeReset(d)— Restarts the timer for duration d, returns true if it was activeC()— 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()andSetTime()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.