The Sometimes utility executes an action occasionally based on configurable rules. It provides a simple way to rate-limit function execution using three filters: First, Every, and Interval. These filters operate as a union (not intersection), meaning the action runs if ANY filter condition is met.
import "golang.org/x/time/rate"type Sometimes struct {
First int // if non-zero, the first N calls to Do will run f
Every int // if non-zero, every Nth call to Do will run f
Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f
}A Sometimes struct controls when an action executes through three configurable fields:
First: If non-zero, the first N calls to Do will run the functionEvery: If non-zero, every Nth call to Do will run the function (starting with the first call)Interval: If non-zero and this duration has elapsed since the last execution, Do will run the functionZero Value Behavior: A zero Sometimes value will execute the action exactly once (on the first call to Do).
Thread Safety: Safe for concurrent use by multiple goroutines.
func (s *Sometimes) Do(f func())Runs the function f as allowed by the First, Every, and Interval filters.
Parameters:
f: The function to execute occasionallyBehavior:
Do always runs ff if allowed by First OR Every OR Interval (union of filters)Do simultaneously, calls block and run seriallyImportant Notes:
Do is intended for lightweight operations since it blocks concurrent callsf calls Do on the same Sometimes, it will deadlockThe filters form a union (not intersection):
Executes the action for the first N calls.
s := rate.Sometimes{First: 5}
// Calls 1-5: execute
// Calls 6+: skipExecutes the action every Nth call, starting with the first call.
s := rate.Sometimes{Every: 3}
// Calls 1, 3, 6, 9, 12...: execute
// Calls 2, 4, 5, 7, 8, 10...: skipExecutes the action if Interval has elapsed since the last execution.
s := rate.Sometimes{Interval: 10 * time.Second}
// Execute if 10+ seconds have passed since last executionWhen multiple filters are specified, the action runs if ANY filter allows it:
s := rate.Sometimes{
First: 3,
Every: 10,
Interval: 1 * time.Minute,
}
// Executes on:
// - Calls 1, 2, 3 (First: 3)
// - Calls 10, 20, 30... (Every: 10)
// - Any call after 1 minute has elapsed since last execution (Interval)import (
"log"
"golang.org/x/time/rate"
"time"
)
// Log every 10 seconds
var statusLogger = rate.Sometimes{
Interval: 10 * time.Second,
}
func processData(data []byte) {
// This will log at most once every 10 seconds
statusLogger.Do(func() {
log.Printf("Processed %d bytes", len(data))
})
// Actual processing logic
process(data)
}import (
"log"
"golang.org/x/time/rate"
"time"
)
// Log first 3 times, then every 30 seconds
var logger = rate.Sometimes{
First: 3,
Interval: 30 * time.Second,
}
func SpammyFunction() {
logger.Do(func() {
log.Println("SpammyFunction called")
})
// Function logic
doWork()
}import (
"fmt"
"golang.org/x/time/rate"
)
// Report progress every 1000 items
var progressReporter = rate.Sometimes{
Every: 1000,
}
func processBatch(items []Item) {
for i, item := range items {
progressReporter.Do(func() {
fmt.Printf("Processed %d items\n", i)
})
processItem(item)
}
}import (
"log"
"golang.org/x/time/rate"
)
func initializeOnce() {
var once rate.Sometimes // Zero value
// This will execute exactly once
once.Do(func() {
log.Println("Initialization happening")
initialize()
})
}import (
"log"
"golang.org/x/time/rate"
"time"
)
// Log first 5 calls for debugging, then every 100 calls or every minute
var debugLogger = rate.Sometimes{
First: 5,
Every: 100,
Interval: 1 * time.Minute,
}
func criticalPath(request Request) {
debugLogger.Do(func() {
log.Printf("Request: %+v", request)
})
handleRequest(request)
}import (
"fmt"
"golang.org/x/time/rate"
"time"
)
type Service struct {
healthReporter rate.Sometimes
}
func NewService() *Service {
return &Service{
healthReporter: rate.Sometimes{
First: 1, // Report immediately on first call
Interval: 5 * time.Minute, // Then every 5 minutes
},
}
}
func (s *Service) ProcessRequest() {
s.healthReporter.Do(func() {
reportHealth()
})
// Process request
}import (
"log"
"golang.org/x/time/rate"
)
// Sample 1 out of every 10,000 events
var sampler = rate.Sometimes{
Every: 10000,
}
func handleEvent(event Event) {
sampler.Do(func() {
log.Printf("Sample event: %+v", event)
sendToAnalytics(event)
})
processEvent(event)
}While sync.Once ensures something happens exactly once, Sometimes provides flexible repetition:
import (
"sync"
"golang.org/x/time/rate"
)
// sync.Once: Executes exactly once, ever
var once sync.Once
once.Do(func() { initialize() })
// rate.Sometimes (zero value): Executes exactly once, ever
var sometimes rate.Sometimes
sometimes.Do(func() { initialize() })
// rate.Sometimes with config: Executes multiple times per rules
var periodic = rate.Sometimes{Interval: 10 * time.Second}
periodic.Do(func() { logStatus() }) // Executes repeatedlySometimes is safe for concurrent use by multiple goroutines. However, note that:
Do serialize execution of f - if multiple goroutines call Do simultaneously, they will block and execute in sequencef should be lightweightSometimes unsuitable for heavy computations in fNEVER call Do from within the function passed to Do on the same Sometimes instance:
// ❌ DEADLOCK - DO NOT DO THIS
var s rate.Sometimes
s.Do(func() {
s.Do(func() { // This will deadlock!
log.Println("This never executes")
})
})Solution: Use separate Sometimes instances or restructure your code:
// ✓ CORRECT
var s1 rate.Sometimes
var s2 rate.Sometimes
s1.Do(func() {
s2.Do(func() { // Different instance, no deadlock
log.Println("This works fine")
})
})Since Do serializes execution, keep f fast:
// ✓ GOOD - Fast operations
logger.Do(func() {
log.Println("Quick message")
})
// ❌ AVOID - Slow operations block other goroutines
logger.Do(func() {
queryDatabase() // Slow
processLargeFile() // Slow
makeNetworkRequest() // Slow
})For high-throughput systems, consider the overhead:
// If this is called millions of times per second,
// the mutex overhead in Do might be noticeable
func hotPath() {
sampler.Do(func() {
collectSample()
})
// ... rest of hot path
}// Log immediately on first call, then periodically
var logger = rate.Sometimes{
First: 1,
Interval: 10 * time.Second,
}// Log first 3 times (for debugging), then every minute
var logger = rate.Sometimes{
First: 3,
Interval: 1 * time.Minute,
}// Log every 10,000th call and also every hour
var logger = rate.Sometimes{
Every: 10000,
Interval: 1 * time.Hour,
}func NewSampler(debug bool) rate.Sometimes {
if debug {
// In debug mode, log first 10 then every 100
return rate.Sometimes{First: 10, Every: 100}
}
// In production, log every 10,000
return rate.Sometimes{Every: 10000}
}Use Sometimes when:
Use Limiter when:
// Sometimes: Simple, automatic filtering
var s = rate.Sometimes{Interval: 10 * time.Second}
func handler() {
s.Do(func() { log.Println("Status") })
// No need to check return value
process()
}
// Limiter: Explicit rate control
var limiter = rate.NewLimiter(10, 5)
func handler() {
if !limiter.Allow() {
return errors.New("rate limit exceeded")
}
process()
}