timeutil is a small Go package that provides serializable timers with automatic
callback execution. The timers behave like the standard time.AfterFunc,
but their deterministic state (start time, duration, and current state) can be
snapshotted and serialized for persistence, inspection, or transfer between
processes.
- Snapshot state — marshal and unmarshal timer state to/from JSON.
- Automatic callback execution — real
time.Timerruns in the background; no manual polling is required. - State-aware — tracks
running,stopped, andexpiredstates. - AfterFunc support — create timers that execute a callback when they expire.
- Thread-safe — all public methods are safe for concurrent use.
- Snapshot support — export lightweight
TimerSnapshotvalues for persistence outside JSON. - Watchdog timer — a separate helper that fires after a period of inactivity unless reset.
go get github.com/ghettovoice/timeutilRequires Go 1.25 or later.
package main
import (
"fmt"
"time"
"github.com/ghettovoice/timeutil"
)
func main() {
// Create a new timer that expires after 5 seconds.
timer := timeutil.NewTimer(5 * time.Second)
// Check whether the timer has expired.
if timer.Expired() {
fmt.Println("Timer has expired")
}
// Time remaining.
fmt.Println("Time remaining:", timer.Left())
// Time elapsed.
fmt.Println("Time elapsed:", timer.Elapsed())
}timer := timeutil.AfterFunc(5*time.Second, func() {
fmt.Println("Timer expired!")
})
// Or set a callback after creation.
timer := timeutil.NewTimer(5 * time.Second)
timer.SetCallback(func() {
fmt.Println("Timer expired!")
})SetCallback automatically executes the callback immediately if the timer has
already expired, and it starts a real time.Timer for timers that are still
running.
// Create a timer.
timer := timeutil.NewTimer(10 * time.Second)
// Serialize to JSON.
data, err := timer.ToJSON()
if err != nil {
panic(err)
}
// Restore later.
restored, err := timeutil.FromJSON(data)
if err != nil {
panic(err)
}
// Callbacks are not serialized; reattach them after restoration.
restored.SetCallback(func() {
fmt.Println("Restored timer expired!")
})The serialized representation is the same as TimerSnapshot:
{
"start_time": "2025-11-03T12:54:05.184256+03:00",
"duration": 5000000000,
"state": "running",
"stop_time": "2025-11-03T12:54:07.184256+03:00"
}start_time— ISO 8601 timestamp when the timer started.duration— duration in nanoseconds.state— one ofrunning,stopped, orexpired.stop_time— present only for stopped timers.
Use FromTime when you need to recreate a timer from saved timing metadata:
startTime := time.Now().Add(-2 * time.Second)
timer := timeutil.FromTime(startTime, 5*time.Second)
// UpdateState checks whether the timer has already expired and triggers the
// callback if one is set.
timer.UpdateState()watchdog := timeutil.NewWatchdog(30*time.Second, func() {
fmt.Println("No activity for 30 seconds")
})
watchdog.Start()
// Later, to keep the watchdog from firing:
watchdog.Reset()
// To pause it while keeping it reusable:
watchdog.Stop()
watchdog.Close()NewWatchdog creates an inactive watchdog; Start or Reset arms it, and a
nonpositive timeout leaves it permanently disabled. Reset always begins a
fresh full countdown, including on a stopped, expired, or never-started
watchdog, and a nil callback is allowed. Stop pauses the watchdog while
keeping it reusable; Close disables it permanently.
Creation:
NewTimer(duration)— create and start a timer immediately.AfterFunc(duration, callback)— create a timer that executes a callback on expiration.FromTime(startTime, duration)— recreate a timer with a specific start time.FromJSON(data)— deserialize a timer from JSON.
State queries:
State()— current timer state.StartTime()— when the timer started.Duration()— configured duration.StopTime()— when the timer was stopped (zero value if not stopped).Expired()— whether the timer has expired.Elapsed()— elapsed time since start.Left()— remaining time until expiration.
Control:
Stop()— stop the timer and prevent callback execution.Reset(duration)— restart the timer with a new duration, preserving any callback.SetCallback(callback)— attach or replace the expiration callback.UpdateState()— re-check expiration and trigger callbacks if needed.
Serialization:
Snapshot()— capture the current state as aTimerSnapshot.SnapshotTimer(timer)— same astimer.Snapshot(), but safe onnil.RestoreTimer(snapshot)— create a new timer from a snapshot.ToJSON()— serialize to JSON.- Implements
json.Marshalerandjson.Unmarshaler.
NewWatchdog(timeout, callback)— create an inactive watchdog.Start()— arm the watchdog to fire aftertimeout; no-op while already armed.Reset()— restart a freshtimeoutcountdown, including on stopped or expired watchdogs.Stop()— cancel the current timer; the watchdog stays reusable viaStartorReset.Close()— disable the watchdog permanently.
- Automatic execution — when a callback is set, a real
time.Timerruns in the background, soUpdateState()is usually not needed. UpdateState()is called automatically during JSON unmarshaling, so timers restored withFromJSONalready reflect the current time.SetCallback()checks for expiration automatically — it runs the callback immediately if the timer is already expired.- Call
UpdateState()manually only when you need to re-check expiration after time has passed, or after creating a timer withFromTimebefore a callback was set. - Callbacks are executed in their own goroutine, like
time.AfterFunc. - Callbacks are not serialized and must be reattached after restoration.
Left()returns0for stopped or expired timers.Reset()preserves any existing callback but restarts the real timer with the new duration.- Watchdog
Stop()andClose()return without waiting for an already admitted callback: once admitted for execution, it may still begin or continue after they return, so callers must coordinate separately; callbacks from different armed generations may overlap.
All public methods on Timer and Watchdog are safe for
concurrent use from multiple goroutines without external synchronization.
Contributions are welcome. Please make sure all checks pass before submitting a pull request:
task checkThis project is licensed under the MIT License. See the LICENSE file for details.