DEV Community

Rez Moss
Rez Moss

Posted on

Practical Applications of Go's Time Package 10/10

Implementing a countdown timer

When working with time-based operations in Go, the time package offers powerful functionality that can simplify complex timing requirements. Let's look at how to implement a practical countdown timer.

A countdown timer is essential for scenarios ranging from limiting the duration of operations to creating time-bound challenges in applications. Here's how you can implement one using Go's time package:

package main import ( "fmt" "time" ) func countdownTimer(duration time.Duration) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() done := make(chan bool) go func() { time.Sleep(duration) done <- true }() remaining := duration fmt.Printf("Countdown started: %s remaining\n", remaining.Round(time.Second)) for { select { case <-done: fmt.Println("Countdown finished!") return case <-ticker.C: remaining -= time.Second fmt.Printf("Time remaining: %s\n", remaining.Round(time.Second)) } } } func main() { // 10-second countdown countdownTimer(10 * time.Second) } 
Enter fullscreen mode Exit fullscreen mode

This implementation leverages goroutines and channels, two powerful Go features that make concurrent operations clean and straightforward. The countdown function uses a ticker to update and display the remaining time every second, while a separate goroutine monitors when the total duration has elapsed.

For more complex scenarios, you might want to add the ability to pause, resume, or cancel the countdown. Here's how you can extend the implementation:

package main import ( "fmt" "time" ) type CountdownTimer struct { duration time.Duration remaining time.Duration ticker *time.Ticker pauseCh chan bool resumeCh chan bool stopCh chan bool isPaused bool } func NewCountdownTimer(duration time.Duration) *CountdownTimer { return &CountdownTimer{ duration: duration, remaining: duration, pauseCh: make(chan bool), resumeCh: make(chan bool), stopCh: make(chan bool), isPaused: false, } } func (c *CountdownTimer) Start() { c.ticker = time.NewTicker(1 * time.Second) go func() { for { select { case <-c.ticker.C: if !c.isPaused { c.remaining -= time.Second fmt.Printf("Time remaining: %s\n", c.remaining.Round(time.Second)) if c.remaining <= 0 { fmt.Println("Countdown finished!") c.ticker.Stop() return } } case <-c.pauseCh: c.isPaused = true fmt.Println("Countdown paused") case <-c.resumeCh: c.isPaused = false fmt.Println("Countdown resumed") case <-c.stopCh: fmt.Println("Countdown stopped") c.ticker.Stop() return } } }() } func (c *CountdownTimer) Pause() { c.pauseCh <- true } func (c *CountdownTimer) Resume() { c.resumeCh <- true } func (c *CountdownTimer) Stop() { c.stopCh <- true } func main() { timer := NewCountdownTimer(10 * time.Second) timer.Start() // Simulate pausing after 3 seconds time.Sleep(3 * time.Second) timer.Pause() // Simulate resuming after 2 seconds time.Sleep(2 * time.Second) timer.Resume() // Let it run to completion time.Sleep(10 * time.Second) } 
Enter fullscreen mode Exit fullscreen mode

This enhanced implementation shows how to build a more flexible countdown timer with control mechanisms. The struct-based approach encapsulates the timer's state and behaviors, making it easier to integrate into larger applications.

A few key points about this countdown timer implementation:

  1. We use Go's duration types (time.Duration) to handle time calculations cleanly
  2. The select statement provides non-blocking channel operations, perfect for handling multiple signals
  3. The ticker ensures regular updates at precise intervals
  4. Goroutines allow the timer to run concurrently with other code

This pattern can be adapted for many time-sensitive applications, from cooking timers to rate limiting and timeout handling in network applications.


Building a task scheduler using time.Ticker

Scheduling recurring tasks is a common requirement in many applications, from periodic data processing to regular health checks. Go's time.Ticker provides an elegant solution for implementing task schedulers with precise timing.

Unlike a simple countdown timer, a task scheduler needs to execute actions at regular intervals, potentially indefinitely. Here's how to build a basic task scheduler using Go's time package:

package main import ( "fmt" "sync" "time" ) type Task struct { ID string Interval time.Duration Action func() } type Scheduler struct { tasks map[string]*Task stop map[string]chan bool mutex sync.RWMutex } func NewScheduler() *Scheduler { return &Scheduler{ tasks: make(map[string]*Task), stop: make(map[string]chan bool), } } func (s *Scheduler) AddTask(task *Task) { s.mutex.Lock() defer s.mutex.Unlock() s.tasks[task.ID] = task s.stop[task.ID] = make(chan bool) go func(t *Task, stop chan bool) { ticker := time.NewTicker(t.Interval) defer ticker.Stop() // Execute immediately on start t.Action() for { select { case <-ticker.C: t.Action() case <-stop: fmt.Printf("Task %s stopped\n", t.ID) return } } }(task, s.stop[task.ID]) fmt.Printf("Task %s added with interval %s\n", task.ID, task.Interval) } func (s *Scheduler) RemoveTask(id string) bool { s.mutex.Lock() defer s.mutex.Unlock() if stopCh, exists := s.stop[id]; exists { stopCh <- true delete(s.stop, id) delete(s.tasks, id) return true } return false } func (s *Scheduler) GetTasks() []*Task { s.mutex.RLock() defer s.mutex.RUnlock() taskList := make([]*Task, 0, len(s.tasks)) for _, task := range s.tasks { taskList = append(taskList, task) } return taskList } func main() { scheduler := NewScheduler() // Add a task that runs every 2 seconds scheduler.AddTask(&Task{ ID: "healthcheck", Interval: 2 * time.Second, Action: func() { fmt.Printf("[%s] Running health check...\n", time.Now().Format(time.RFC3339)) }, }) // Add a task that runs every 5 seconds scheduler.AddTask(&Task{ ID: "cleanup", Interval: 5 * time.Second, Action: func() { fmt.Printf("[%s] Performing cleanup...\n", time.Now().Format(time.RFC3339)) }, }) // Let tasks run for 12 seconds time.Sleep(12 * time.Second) // Remove the health check task scheduler.RemoveTask("healthcheck") // Let the remaining task run for a bit longer time.Sleep(6 * time.Second) } 
Enter fullscreen mode Exit fullscreen mode

This scheduler implementation demonstrates several important Go patterns:

  1. Encapsulation: The scheduler encapsulates the complexities of managing multiple concurrent tasks.
  2. Concurrency: Each task runs in its own goroutine, allowing tasks to execute independently.
  3. Synchronization: Mutex locks prevent race conditions when modifying the task collection.
  4. Resource Management: Proper cleanup occurs when tasks are removed.

For real-world applications, you might want to enhance this scheduler with features like:

// Add to the Scheduler struct type Scheduler struct { // existing fields... taskStats map[string]TaskStats } type TaskStats struct { LastRun time.Time ExecutionCount int AverageRuntime time.Duration TotalRuntime time.Duration } // Then modify the AddTask method to track stats func (s *Scheduler) AddTask(task *Task) { // existing code... go func(t *Task, stop chan bool) { ticker := time.NewTicker(t.Interval) defer ticker.Stop() s.taskStats[t.ID] = TaskStats{} // Execute immediately on start startTime := time.Now() t.Action() runTime := time.Since(startTime) s.mutex.Lock() stats := s.taskStats[t.ID] stats.LastRun = startTime stats.ExecutionCount = 1 stats.AverageRuntime = runTime stats.TotalRuntime = runTime s.taskStats[t.ID] = stats s.mutex.Unlock() for { select { case <-ticker.C: startTime := time.Now() t.Action() runTime := time.Since(startTime) s.mutex.Lock() stats := s.taskStats[t.ID] stats.LastRun = startTime stats.ExecutionCount++ stats.TotalRuntime += runTime stats.AverageRuntime = stats.TotalRuntime / time.Duration(stats.ExecutionCount) s.taskStats[t.ID] = stats s.mutex.Unlock() case <-stop: fmt.Printf("Task %s stopped\n", t.ID) return } } }(task, s.stop[task.ID]) } 
Enter fullscreen mode Exit fullscreen mode

When implementing a scheduler in production systems, consider these additional points:

  1. Jitter: For distributed systems, add slight randomness to intervals to prevent thundering herd problems.
  2. Error Handling: Wrap task executions in recovery blocks to prevent a failing task from crashing the scheduler.
  3. Persistence: For critical tasks, you might want to persist task schedules to restart them after application restarts.
  4. Dynamic Intervals: Allow tasks to change their execution intervals at runtime based on conditions.

The time.Ticker approach works well for most scheduling needs, but for more complex scenarios, you might consider using a cron-like library that supports calendar-based scheduling.


Converting and displaying timestamps in human-readable formats

Working with timestamps is a common requirement in Go applications, whether you're logging events, displaying user activity, or managing time-sensitive data. Go's time package provides robust tools for formatting and parsing timestamps in various formats.

Parsing Time Strings

Let's start with parsing time strings into Go's time.Time objects:

package main import ( "fmt" "time" ) func main() { // Parse a timestamp from a standard format timestamp, err := time.Parse(time.RFC3339, "2023-10-15T14:30:45Z") if err != nil { fmt.Println("Error parsing time:", err) return } fmt.Println("Parsed time:", timestamp) // Parse a custom format // The reference time is: Mon Jan 2 15:04:05 MST 2006 customTime, err := time.Parse("2006-01-02 15:04:05", "2023-10-15 14:30:45") if err != nil { fmt.Println("Error parsing custom time:", err) return } fmt.Println("Custom parsed time:", customTime) // Parse time with timezone loc, err := time.LoadLocation("America/New_York") if err != nil { fmt.Println("Error loading location:", err) return } nyTime, err := time.ParseInLocation("2006-01-02 15:04:05", "2023-10-15 14:30:45", loc) if err != nil { fmt.Println("Error parsing time with location:", err) return } fmt.Println("New York time:", nyTime) } 
Enter fullscreen mode Exit fullscreen mode

Formatting Time Objects

Once you have a time.Time object, you can format it in various human-readable ways:

package main import ( "fmt" "time" ) func main() { // Get current time now := time.Now() // Standard formats fmt.Println("RFC3339:", now.Format(time.RFC3339)) fmt.Println("RFC822:", now.Format(time.RFC822)) fmt.Println("Kitchen time:", now.Format(time.Kitchen)) // Custom formats fmt.Println("Custom date:", now.Format("Monday, January 2, 2006")) fmt.Println("Short date:", now.Format("2006-01-02")) fmt.Println("Custom time:", now.Format("15:04:05")) fmt.Println("With timezone:", now.Format("2006-01-02 15:04:05 MST")) // Format with different timezone loc, _ := time.LoadLocation("Europe/Paris") parisTime := now.In(loc) fmt.Println("Paris time:", parisTime.Format("2006-01-02 15:04:05 MST")) } 
Enter fullscreen mode Exit fullscreen mode

Creating User-Friendly Relative Times

Often, displaying relative times like "5 minutes ago" or "2 days ago" provides a better user experience:

package main import ( "fmt" "math" "time" ) func timeAgo(t time.Time) string { now := time.Now() duration := now.Sub(t) seconds := int(duration.Seconds()) minutes := int(duration.Minutes()) hours := int(duration.Hours()) days := int(hours / 24) if seconds < 60 { return fmt.Sprintf("%d seconds ago", seconds) } else if minutes < 60 { return fmt.Sprintf("%d minutes ago", minutes) } else if hours < 24 { return fmt.Sprintf("%d hours ago", hours) } else if days < 30 { return fmt.Sprintf("%d days ago", days) } else if days < 365 { months := int(math.Floor(float64(days) / 30)) return fmt.Sprintf("%d months ago", months) } years := int(math.Floor(float64(days) / 365)) return fmt.Sprintf("%d years ago", years) } func main() { times := []time.Time{ time.Now().Add(-30 * time.Second), time.Now().Add(-10 * time.Minute), time.Now().Add(-5 * time.Hour), time.Now().Add(-3 * 24 * time.Hour), time.Now().Add(-60 * 24 * time.Hour), time.Now().Add(-400 * 24 * time.Hour), } for _, t := range times { fmt.Printf("Time: %s -> %s\n", t.Format(time.RFC3339), timeAgo(t)) } } 
Enter fullscreen mode Exit fullscreen mode

Working with Different Time Zones

Time zone handling is often tricky but essential for global applications:

package main import ( "fmt" "time" ) func displayTimeInDifferentTimezones(t time.Time) { locations := []string{ "UTC", "America/New_York", "Europe/London", "Asia/Tokyo", "Australia/Sydney", } fmt.Printf("Original time: %s\n", t.Format(time.RFC3339)) for _, locName := range locations { loc, err := time.LoadLocation(locName) if err != nil { fmt.Printf("Error loading location %s: %v\n", locName, err) continue } localTime := t.In(loc) fmt.Printf("%15s: %s\n", locName, localTime.Format("2006-01-02 15:04:05 MST")) } } func main() { // Current time now := time.Now() displayTimeInDifferentTimezones(now) // Meeting time meetingTime := time.Date(2023, 10, 20, 15, 0, 0, 0, time.UTC) fmt.Println("\nMeeting time in different timezones:") displayTimeInDifferentTimezones(meetingTime) } 
Enter fullscreen mode Exit fullscreen mode

Practical Example: Formatting Database Timestamps

When retrieving timestamps from databases, you often need to convert them to appropriate formats:

package main import ( "fmt" "time" ) // Simulate a database record type UserActivity struct { UserID int ActivityType string Timestamp time.Time } func formatActivityTimestamp(activity UserActivity) string { now := time.Now() activityTime := activity.Timestamp // If it happened today, show the time if now.Year() == activityTime.Year() && now.Month() == activityTime.Month() && now.Day() == activityTime.Day() { return fmt.Sprintf("Today at %s", activityTime.Format("3:04 PM")) } // If it happened yesterday, say "Yesterday" yesterday := now.AddDate(0, 0, -1) if yesterday.Year() == activityTime.Year() && yesterday.Month() == activityTime.Month() && yesterday.Day() == activityTime.Day() { return fmt.Sprintf("Yesterday at %s", activityTime.Format("3:04 PM")) } // If it happened this year, show month and day if now.Year() == activityTime.Year() { return activityTime.Format("Jan 2 at 3:04 PM") } // Otherwise show the full date return activityTime.Format("Jan 2, 2006 at 3:04 PM") } func main() { activities := []UserActivity{ {1, "login", time.Now()}, {2, "purchase", time.Now().Add(-24 * time.Hour)}, {3, "signup", time.Now().Add(-5 * 24 * time.Hour)}, {4, "login", time.Now().AddDate(-1, 0, 0)}, } for _, activity := range activities { fmt.Printf("User %d %s %s\n", activity.UserID, activity.ActivityType, formatActivityTimestamp(activity)) } } 
Enter fullscreen mode Exit fullscreen mode

These examples demonstrate the versatility of Go's time package for parsing, formatting, and displaying timestamps in human-readable formats. By leveraging these functions, you can create more intuitive time representations in your applications, enhancing user experience and making temporal data more accessible.


Logging and timestamping events correctly

Proper logging and timestamping are essential for debugging, monitoring, and auditing applications. Go's time package provides the necessary tools to ensure accurate and consistent timestamps in your log entries.

Basic Event Logging with Timestamps

Let's start with a simple logging utility that includes proper timestamps:

package main import ( "fmt" "log" "os" "time" ) // LogLevel represents the severity of a log entry type LogLevel int const ( DEBUG LogLevel = iota INFO WARNING ERROR FATAL ) // String representation of log levels func (l LogLevel) String() string { return [...]string{"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"}[l] } // Logger is a simple logging utility with timestamps type Logger struct { level LogLevel logger *log.Logger } // NewLogger creates a new Logger instance func NewLogger(level LogLevel) *Logger { return &Logger{ level: level, logger: log.New(os.Stdout, "", 0), // No prefix or flags, we'll format ourselves } } // Log logs a message with the specified level func (l *Logger) Log(level LogLevel, format string, args ...interface{}) { if level < l.level { return // Skip messages below the configured level } // Get current time with microsecond precision now := time.Now() timestamp := now.Format("2006-01-02T15:04:05.000000Z07:00") // Format the message message := fmt.Sprintf(format, args...) // Log the timestamped message l.logger.Printf("[%s] [%s] %s", timestamp, level, message) } // Convenience methods for different log levels func (l *Logger) Debug(format string, args ...interface{}) { l.Log(DEBUG, format, args...) } func (l *Logger) Info(format string, args ...interface{}) { l.Log(INFO, format, args...) } func (l *Logger) Warning(format string, args ...interface{}) { l.Log(WARNING, format, args...) } func (l *Logger) Error(format string, args ...interface{}) { l.Log(ERROR, format, args...) } func (l *Logger) Fatal(format string, args ...interface{}) { l.Log(FATAL, format, args...) os.Exit(1) } func main() { logger := NewLogger(DEBUG) // Simulate application events logger.Info("Application started") // Simulate processing with timing start := time.Now() time.Sleep(100 * time.Millisecond) // Simulate work logger.Debug("Processing took %v", time.Since(start)) // Log various events logger.Info("User login successful: user_id=%d", 1234) logger.Warning("Database connection pool reaching limit: %d/%d", 8, 10) logger.Error("Failed to process payment: %s", "insufficient funds") // Don't actually call Fatal in this example // logger.Fatal("Unrecoverable error: %s", "database connection lost") } 
Enter fullscreen mode Exit fullscreen mode

Logging Across Time Zones

For distributed systems or applications serving global users, handling time zones correctly is crucial:

package main import ( "fmt" "log" "os" "time" ) // TimeFormat defines different timestamp formats for logs type TimeFormat int const ( UTC TimeFormat = iota Local ISO8601 RFC3339Nano ) type TimestampedLogger struct { logger *log.Logger format TimeFormat loc *time.Location } func NewTimestampedLogger(format TimeFormat, location *time.Location) *TimestampedLogger { return &TimestampedLogger{ logger: log.New(os.Stdout, "", 0), format: format, loc: location, } } func (t *TimestampedLogger) formatTime(tm time.Time) string { // Convert to the desired time zone zonedTime := tm if t.loc != nil { zonedTime = tm.In(t.loc) } // Format according to the specified format switch t.format { case UTC: return zonedTime.In(time.UTC).Format("2006-01-02 15:04:05.000000 UTC") case Local: return zonedTime.Format("2006-01-02 15:04:05.000000 MST") case ISO8601: return zonedTime.Format("2006-01-02T15:04:05.000000-07:00") case RFC3339Nano: return zonedTime.Format(time.RFC3339Nano) default: return zonedTime.Format(time.RFC3339) } } func (t *TimestampedLogger) Log(message string) { timestamp := t.formatTime(time.Now()) t.logger.Printf("[%s] %s", timestamp, message) } func main() { // Create loggers with different time formats utcLogger := NewTimestampedLogger(UTC, nil) localLogger := NewTimestampedLogger(Local, nil) // Load Tokyo time zone tokyo, _ := time.LoadLocation("Asia/Tokyo") tokyoLogger := NewTimestampedLogger(ISO8601, tokyo) // Log the same event with different time formats utcLogger.Log("Event logged in UTC time") localLogger.Log("Same event logged in local time") tokyoLogger.Log("Same event logged in Tokyo time") } 
Enter fullscreen mode Exit fullscreen mode

Creating a Structured Logger with Performance Metrics

For production systems, structured logging with performance metrics can be invaluable:

package main import ( "encoding/json" "fmt" "os" "runtime" "time" ) type LogEntry struct { Timestamp string `json:"timestamp"` Level string `json:"level"` Message string `json:"message"` Context map[string]interface{} `json:"context,omitempty"` Performance *PerformanceMetrics `json:"performance,omitempty"` } type PerformanceMetrics struct { Duration string `json:"duration,omitempty"` MemoryUsage string `json:"memory_usage,omitempty"` GoRoutines int `json:"goroutines,omitempty"` } type StructuredLogger struct { AppName string } func NewStructuredLogger(appName string) *StructuredLogger { return &StructuredLogger{AppName: appName} } func (s *StructuredLogger) Log(level, message string, context map[string]interface{}, includePerf bool) { entry := LogEntry{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: level, Message: message, Context: context, } // Add app name to context if entry.Context == nil { entry.Context = make(map[string]interface{}) } entry.Context["app"] = s.AppName // Include performance metrics if requested if includePerf { var mem runtime.MemStats runtime.ReadMemStats(&mem) entry.Performance = &PerformanceMetrics{ GoRoutines: runtime.NumGoroutine(), MemoryUsage: fmt.Sprintf("%.2f MB", float64(mem.Alloc)/1024/1024), } } // Marshal to JSON and write to stdout jsonData, _ := json.Marshal(entry) fmt.Fprintln(os.Stdout, string(jsonData)) } func (s *StructuredLogger) LogWithDuration(level, message string, context map[string]interface{}, start time.Time) { duration := time.Since(start) if context == nil { context = make(map[string]interface{}) } // Include performance metrics with duration var mem runtime.MemStats runtime.ReadMemStats(&mem) entry := LogEntry{ Timestamp: time.Now().UTC().Format(time.RFC3339Nano), Level: level, Message: message, Context: context, Performance: &PerformanceMetrics{ Duration: duration.String(), GoRoutines: runtime.NumGoroutine(), MemoryUsage: fmt.Sprintf("%.2f MB", float64(mem.Alloc)/1024/1024), }, } // Add app name to context entry.Context["app"] = s.AppName // Marshal to JSON and write to stdout jsonData, _ := json.Marshal(entry) fmt.Fprintln(os.Stdout, string(jsonData)) } func main() { logger := NewStructuredLogger("demo-app") // Simple log logger.Log("INFO", "Application started", nil, false) // Log with context userContext := map[string]interface{}{ "user_id": 12345, "action": "login", "source": "api", } logger.Log("INFO", "User logged in", userContext, true) // Measure operation duration start := time.Now() time.Sleep(150 * time.Millisecond) // Simulate work processContext := map[string]interface{}{ "items_processed": 1000, "batch_id": "batch-2023-10", } logger.LogWithDuration("INFO", "Batch processing completed", processContext, start) } 
Enter fullscreen mode Exit fullscreen mode

Best Practices for Logging and Timestamping

When implementing logging in Go applications, follow these best practices:

  1. Use UTC for server-side logs: This ensures consistency across distributed services and avoids daylight saving time complications.
// Always log in UTC for backend services timestamp := time.Now().UTC().Format(time.RFC3339Nano) 
Enter fullscreen mode Exit fullscreen mode
  1. Include timezone information in timestamps: This helps avoid ambiguity.
// Include timezone offset in logs timestamp := time.Now().Format("2006-01-02T15:04:05-07:00") 
Enter fullscreen mode Exit fullscreen mode
  1. Maintain precision: Include milliseconds or microseconds for performance-sensitive applications.
// High precision timestamp for performance logging timestamp := time.Now().Format("2006-01-02T15:04:05.000000Z07:00") 
Enter fullscreen mode Exit fullscreen mode
  1. Be consistent: Use the same timestamp format across your application.
// Define a constant for your timestamp format const TimeFormat = "2006-01-02T15:04:05.000Z07:00" // Use it consistently timestamp := time.Now().Format(TimeFormat) 
Enter fullscreen mode Exit fullscreen mode
  1. Context is key: Include relevant context with your timestamps, such as request IDs.
type LogContext struct { Timestamp string `json:"timestamp"` RequestID string `json:"request_id"` UserID int `json:"user_id,omitempty"` // other fields... } 
Enter fullscreen mode Exit fullscreen mode

By implementing these patterns and best practices, you can create a robust logging system that accurately timestamps events, facilitating debugging, monitoring, and auditing of your Go applications.


Managing time-based expiration and caching

Time-based expiration and caching mechanisms are essential components of many applications, from API rate limiting to session management. Go's time package provides the tools needed to implement effective caching strategies with precise expiration control.

Building a Simple In-Memory Cache with Expiration

Let's start with a basic in-memory cache that automatically expires items after a specified duration:

package main import ( "fmt" "sync" "time" ) // CacheItem represents an item in the cache with expiration time type CacheItem struct { Value interface{} Expiration time.Time } // Cache is a simple in-memory cache with expiration type Cache struct { items map[string]CacheItem mu sync.RWMutex } // NewCache creates a new cache with automatic cleanup func NewCache(cleanupInterval time.Duration) *Cache { cache := &Cache{ items: make(map[string]CacheItem), } // Start the cleanup routine if a positive interval is provided if cleanupInterval > 0 { go cache.startCleanupTimer(cleanupInterval) } return cache } // Set adds an item to the cache with a specified expiration duration func (c *Cache) Set(key string, value interface{}, duration time.Duration) { c.mu.Lock() defer c.mu.Unlock() var expiration time.Time if duration > 0 { expiration = time.Now().Add(duration) } c.items[key] = CacheItem{ Value: value, Expiration: expiration, } } // Get retrieves an item from the cache // Returns the item and a bool indicating if the item was found func (c *Cache) Get(key string) (interface{}, bool) { c.mu.RLock() defer c.mu.RUnlock() item, found := c.items[key] if !found { return nil, false } // Check if the item has expired if !item.Expiration.IsZero() && time.Now().After(item.Expiration) { return nil, false } return item.Value, true } // Delete removes an item from the cache func (c *Cache) Delete(key string) { c.mu.Lock() defer c.mu.Unlock() delete(c.items, key) } // startCleanupTimer starts a timer to periodically clean up expired items func (c *Cache) startCleanupTimer(interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: c.cleanup() } } } // cleanup removes expired items from the cache func (c *Cache) cleanup() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() for key, item := range c.items { if !item.Expiration.IsZero() && now.After(item.Expiration) { delete(c.items, key) } } } func main() { // Create a cache with cleanup every 5 seconds cache := NewCache(5 * time.Second) // Add items with different expiration times cache.Set("short-lived", "I expire quickly", 2*time.Second) cache.Set("long-lived", "I stay around longer", 10*time.Second) cache.Set("immortal", "I live forever", 0) // 0 means no expiration // Check initial values fmt.Println("--- Initial state ---") printCacheItem(cache, "short-lived") printCacheItem(cache, "long-lived") printCacheItem(cache, "immortal") // Wait for the short-lived item to expire fmt.Println("\n--- After 3 seconds ---") time.Sleep(3 * time.Second) printCacheItem(cache, "short-lived") // Should be expired printCacheItem(cache, "long-lived") // Should still exist printCacheItem(cache, "immortal") // Should still exist // Wait for the long-lived item to expire fmt.Println("\n--- After 8 more seconds ---") time.Sleep(8 * time.Second) printCacheItem(cache, "short-lived") // Should be expired printCacheItem(cache, "long-lived") // Should be expired printCacheItem(cache, "immortal") // Should still exist } func printCacheItem(cache *Cache, key string) { value, found := cache.Get(key) if found { fmt.Printf("Key: %s, Value: %v\n", key, value) } else { fmt.Printf("Key: %s not found or expired\n", key) } } 
Enter fullscreen mode Exit fullscreen mode

Implementing Cache with Sliding Expiration

For many applications, it's useful to implement a sliding expiration strategy, where the expiration time is reset on each access:

package main import ( "fmt" "sync" "time" ) // SlidingCacheItem represents an item with sliding expiration type SlidingCacheItem struct { Value interface{} Duration time.Duration // Store the original duration Expiration time.Time } // SlidingCache implements a cache with sliding expiration type SlidingCache struct { items map[string]SlidingCacheItem mu sync.RWMutex } // NewSlidingCache creates a new cache with sliding expiration func NewSlidingCache(cleanupInterval time.Duration) *SlidingCache { cache := &SlidingCache{ items: make(map[string]SlidingCacheItem), } // Start the cleanup routine if a positive interval is provided if cleanupInterval > 0 { go cache.startCleanupTimer(cleanupInterval) } return cache } // Set adds an item to the cache with the specified duration func (c *SlidingCache) Set(key string, value interface{}, duration time.Duration) { c.mu.Lock() defer c.mu.Unlock() expiration := time.Now().Add(duration) c.items[key] = SlidingCacheItem{ Value: value, Duration: duration, Expiration: expiration, } } // Get retrieves an item and resets its expiration time func (c *SlidingCache) Get(key string) (interface{}, bool) { c.mu.Lock() defer c.mu.Unlock() item, found := c.items[key] if !found { return nil, false } // Check if the item has expired if time.Now().After(item.Expiration) { delete(c.items, key) return nil, false } // Reset expiration time (sliding window) if item.Duration > 0 { item.Expiration = time.Now().Add(item.Duration) c.items[key] = item } return item.Value, true } // Rest of the implementation (Delete, cleanup, etc.) is similar to the basic cache // ... func (c *SlidingCache) startCleanupTimer(interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: c.cleanup() } } } func (c *SlidingCache) cleanup() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() for key, item := range c.items { if now.After(item.Expiration) { delete(c.items, key) } } } func main() { // Create a sliding cache with cleanup every 1 second cache := NewSlidingCache(1 * time.Second) // Add an item with 5-second expiration cache.Set("session", "user-123", 5*time.Second) fmt.Println("Added session to cache at", time.Now().Format("15:04:05")) // Wait 3 seconds and access the item time.Sleep(3 * time.Second) if value, found := cache.Get("session"); found { fmt.Println("At", time.Now().Format("15:04:05"), "- Found session:", value) fmt.Println("Expiration extended by 5 seconds") } // Wait 3 more seconds - the item should still be there time.Sleep(3 * time.Second) if value, found := cache.Get("session"); found { fmt.Println("At", time.Now().Format("15:04:05"), "- Found session:", value) fmt.Println("Expiration extended again by 5 seconds") } else { fmt.Println("At", time.Now().Format("15:04:05"), "- Session expired") } // Wait 6 seconds without accessing - should expire time.Sleep(6 * time.Second) if _, found := cache.Get("session"); found { fmt.Println("At", time.Now().Format("15:04:05"), "- Session still active") } else { fmt.Println("At", time.Now().Format("15:04:05"), "- Session expired") } } 
Enter fullscreen mode Exit fullscreen mode

TTL Map for API Rate Limiting

A common use case for time-based expiration is API rate limiting. Here's a simple implementation:

package main import ( "fmt" "sync" "time" ) // RateLimiter implements a simple token bucket rate limiter type RateLimiter struct { limits map[string]Limit mu sync.RWMutex cleanupInt time.Duration } // Limit represents rate limit information for a key type Limit struct { Count int // Current count of requests ResetAt time.Time // When the limit resets MaxCount int // Maximum allowed requests per window WindowSize time.Duration // Time window for rate limiting } // NewRateLimiter creates a new rate limiter with the specified cleanup interval func NewRateLimiter(cleanupInterval time.Duration) *RateLimiter { limiter := &RateLimiter{ limits: make(map[string]Limit), cleanupInt: cleanupInterval, } // Start background cleanup go limiter.startCleanup() return limiter } // IsAllowed checks if a request is allowed for the given key func (rl *RateLimiter) IsAllowed(key string, maxCount int, windowSize time.Duration) (bool, Limit) { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() // Get existing limit or create a new one limit, exists := rl.limits[key] if !exists || now.After(limit.ResetAt) { // Create a new limit limit = Limit{ Count: 1, // This request counts as the first ResetAt: now.Add(windowSize), MaxCount: maxCount, WindowSize: windowSize, } rl.limits[key] = limit return true, limit } // Increment counter and check if allowed if limit.Count < limit.MaxCount { limit.Count++ rl.limits[key] = limit return true, limit } return false, limit } // GetRateLimit returns the current rate limit for a key func (rl *RateLimiter) GetRateLimit(key string) (Limit, bool) { rl.mu.RLock() defer rl.mu.RUnlock() limit, exists := rl.limits[key] return limit, exists } // startCleanup periodically removes expired rate limits func (rl *RateLimiter) startCleanup() { ticker := time.NewTicker(rl.cleanupInt) defer ticker.Stop() for { select { case <-ticker.C: rl.cleanup() } } } // cleanup removes expired rate limits func (rl *RateLimiter) cleanup() { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() for key, limit := range rl.limits { if now.After(limit.ResetAt) { delete(rl.limits, key) } } } func main() { // Create a rate limiter with cleanup every 5 seconds limiter := NewRateLimiter(5 * time.Second) // Simulate API requests for user1 userKey := "user1" // Allow 5 requests per 10 seconds maxRequests := 5 windowSize := 10 * time.Second fmt.Println("Simulating API requests for", userKey) // Make 7 requests (exceeding the limit) for i := 1; i <= 7; i++ { allowed, limit := limiter.IsAllowed(userKey, maxRequests, windowSize) fmt.Printf("Request %d: Allowed=%v, Count=%d/%d, Reset in %v\n", i, allowed, limit.Count, limit.MaxCount, time.Until(limit.ResetAt).Round(time.Second)) time.Sleep(1 * time.Second) } // Wait for rate limit to reset fmt.Println("\nWaiting for rate limit to reset...") time.Sleep(10 * time.Second) // Make another request after reset allowed, limit := limiter.IsAllowed(userKey, maxRequests, windowSize) fmt.Printf("After reset: Allowed=%v, Count=%d/%d, Reset in %v\n", allowed, limit.Count, limit.MaxCount, time.Until(limit.ResetAt).Round(time.Second)) } 
Enter fullscreen mode Exit fullscreen mode

Advanced Caching with Time-Based Eviction Policies

For production systems, you might need more sophisticated caching strategies:

package main import ( "fmt" "sync" "time" ) // EvictionPolicy defines how items are evicted from the cache type EvictionPolicy int const ( LRU EvictionPolicy = iota // Least Recently Used LFU // Least Frequently Used FIFO // First In First Out ) // AdvancedCacheItem represents an item in the advanced cache type AdvancedCacheItem struct { Key string Value interface{} Expiration time.Time Created time.Time LastAccess time.Time AccessCount int } // AdvancedCache implements a cache with multiple eviction policies type AdvancedCache struct { items map[string]*AdvancedCacheItem mu sync.RWMutex maxItems int policy EvictionPolicy itemsList []*AdvancedCacheItem // For FIFO ordering } // NewAdvancedCache creates a new advanced cache func NewAdvancedCache(maxItems int, policy EvictionPolicy, cleanupInterval time.Duration) *AdvancedCache { cache := &AdvancedCache{ items: make(map[string]*AdvancedCacheItem), maxItems: maxItems, policy: policy, itemsList: make([]*AdvancedCacheItem, 0, maxItems), } // Start the cleanup routine go cache.startCleanupTimer(cleanupInterval) return cache } // Set adds or updates an item in the cache func (c *AdvancedCache) Set(key string, value interface{}, duration time.Duration) { c.mu.Lock() defer c.mu.Unlock() now := time.Now() var expiration time.Time if duration > 0 { expiration = now.Add(duration) } // Check if the item already exists if item, found := c.items[key]; found { item.Value = value item.Expiration = expiration item.LastAccess = now item.AccessCount++ return } // Create a new item item := &AdvancedCacheItem{ Key: key, Value: value, Expiration: expiration, Created: now, LastAccess: now, AccessCount: 1, } // Check if we need to evict an item if len(c.items) >= c.maxItems { c.evict() } // Add the new item c.items[key] = item c.itemsList = append(c.itemsList, item) } // Get retrieves an item from the cache func (c *AdvancedCache) Get(key string) (interface{}, bool) { c.mu.Lock() defer c.mu.Unlock() item, found := c.items[key] if !found { return nil, false } // Check if the item has expired if !item.Expiration.IsZero() && time.Now().After(item.Expiration) { c.deleteItem(key) return nil, false } // Update access metrics item.LastAccess = time.Now() item.AccessCount++ return item.Value, true } // deleteItem removes an item from the cache (must be called with lock held) func (c *AdvancedCache) deleteItem(key string) { delete(c.items, key) // Remove from items list (for FIFO) for i, item := range c.itemsList { if item.Key == key { c.itemsList = append(c.itemsList[:i], c.itemsList[i+1:]...) break } } } // evict removes an item based on the eviction policy func (c *AdvancedCache) evict() { if len(c.items) == 0 { return } var keyToEvict string switch c.policy { case LRU: // Find least recently used item var oldest time.Time first := true for k, item := range c.items { if first || item.LastAccess.Before(oldest) { oldest = item.LastAccess keyToEvict = k first = false } } case LFU: // Find least frequently used item var minCount int first := true for k, item := range c.items { if first || item.AccessCount < minCount { minCount = item.AccessCount keyToEvict = k first = false } } case FIFO: // First in, first out if len(c.itemsList) > 0 { keyToEvict = c.itemsList[0].Key } } // Evict the selected item if keyToEvict != "" { c.deleteItem(keyToEvict) } } // startCleanupTimer starts a timer to periodically clean up expired items func (c *AdvancedCache) startCleanupTimer(interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: c.cleanup() } } } // cleanup removes expired items from the cache func (c *AdvancedCache) cleanup() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() for key, item := range c.items { if !item.Expiration.IsZero() && now.After(item.Expiration) { c.deleteItem(key) } } } func main() { // Create an LRU cache with max 3 items cache := NewAdvancedCache(3, LRU, 5*time.Second) // Add items cache.Set("a", "Value A", 10*time.Second) cache.Set("b", "Value B", 10*time.Second) cache.Set("c", "Value C", 10*time.Second) // Access some items to change their LRU status cache.Get("a") cache.Get("b") cache.Get("b") // Add a new item, should evict "c" (least recently used) cache.Set("d", "Value D", 10*time.Second) // Check which items remain fmt.Println("After eviction:") checkKeys := []string{"a", "b", "c", "d"} for _, key := range checkKeys { if val, found := cache.Get(key); found { fmt.Printf("Key %s: %v (found)\n", key, val) } else { fmt.Printf("Key %s: not found\n", key) } } } 
Enter fullscreen mode Exit fullscreen mode

Best Practices for Time-Based Caching

  1. Select the right eviction policy for your workload:

    • LRU works well for most general-purpose caches
    • LFU might be better for slowly changing popularity patterns
    • FIFO is simpler but less adaptive to usage patterns
  2. Consider memory usage when implementing caches:

    • Set reasonable maximum item counts or sizes
    • Implement periodic cleanup to prevent memory leaks
    • Profile memory usage under load
  3. Choose appropriate TTLs for different types of data:

    • Short TTLs for frequently changing data
    • Longer TTLs for relatively static data
    • Consider sliding expiration for session data
  4. Use caching with idempotent operations to avoid consistency issues:

    • Cache GET responses more aggressively
    • Be cautious with POST/PUT/DELETE caching
  5. Implement stale-while-revalidate patterns for better performance:

    • Continue serving stale content while fetching fresh data
    • Update the cache in the background
  6. Add jitter to expiration times to prevent thundering herd problems:

    • Slightly randomize TTLs to distribute cache refreshes
    • Implement cache stampede protection

By following these best practices and leveraging Go's time package, you can build robust caching mechanisms that optimize performance while ensuring freshness of your application's data.

Top comments (0)