agentsclimarketplace

Go time

Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-time

Guides the time package correctly — never compare time.Time with == (it compares the wall instant AND the monotonic reading AND the Location, so two values for the same instant can be unequal); use t.Equal/t.Before/t.After. Reference-layout formatting where the magic constant is the date "Mon Jan 2 15:04:05 MST 2006" (so "2006-01-02", not strftime %Y-%m-%d). time.Duration is an int64 of nanoseconds, so a bare integer is nanoseconds (time.Sleep(1) sleeps 1ns) — write typed literals like 5*time.Second. Stop tickers and don't leak time.After in hot loops; measure elapsed with time.Since (monotonic), not wall-clock subtraction; check the time.Parse error. Auto-invokes when writing or editing time.Time comparisons, time formatting/parsing layouts, time.Duration literals, timers/tickers, measuring elapsed time, or on "why does this time format look wrong" / "why is == on times false" requests. The layout is a date, not a format string; the surprises are all in the defaults.From its SKILL.md

Install
npx -y skills add ctoth/golang-skills-plugin --skill go-time

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.2 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it

Go Time

"In general, prefer t.Equal(u) to t == u, since t.Equal uses the most accurate comparison available and correctly handles the case when only one of its arguments has a monotonic clock reading." — time package

"The Time returned by time.Now contains both a wall clock reading and a monotonic clock reading ... later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading." — time package, Monotonic Clocks

time.Time is not a plain instant. It is a struct carrying a wall-clock reading, a Location, and (when it comes from time.Now) a monotonic-clock reading — and time's formatting layout is a date, not a %-directive string. Every recurring bug below is one of those facts surprising the author. The policy root (go-idiomatic-discipline) bans the swallowed error this skill's §6 surfaces from time.Parse; this skill owns the time surface.


1. Never Compare time.Time With ==

== on a struct compares every field. For time.Time that means the wall reading, the monotonic reading, and the Location pointer — not just the instant. "Note that the Go == operator compares not just the time instant but also the Location and the monotonic clock reading" (time). So two values naming the same instant can be unequal: one from time.Now() carries a monotonic reading; the same value after a JSON/binary round-trip does not, because "the serialized forms ... omit the monotonic clock reading" (time, Monotonic Clocks).

// WRONG — == also compares monotonic reading + Location; same instant, but false
if t == other { ... }

// RIGHT — Equal compares the instant, handling a missing monotonic reading correctly
if t.Equal(other) { ... }
if t.Before(deadline) { ... }
if t.After(start) { ... }

"If Times t and u both contain monotonic clock readings, the operations t.After(u), t.Before(u), t.Equal(u), t.Compare(u), and t.Sub(u) are carried out using the monotonic clock readings alone" (time, Monotonic Clocks). This is verified against Go 1.26 in the reference test: a time.Now() value and its marshal/unmarshal round-trip name the same instant, yet withMono == noMono is false while withMono.Equal(noMono) is true. The same hazard makes a raw time.Time a bad map or database key — strip the monotonic reading first (t.Round(0)) or key on t.UnixNano().


2. The Layout Is a Date, Not strftime

Go's format/parse layout is the reference time Mon Jan 2 15:04:05 MST 2006 written in your desired arrangement — Unix time 1136239445, i.e. 01/02 03:04:05PM '06 -0700 (time). You do not write %Y-%m-%d; you write the date 2006-01-02. The mnemonic is the count-up 1 2 3 4 5 6 7 → month=01, day=02, hour=15 (or 03), minute=04, second=05, year=06, zone=-0700.

// WRONG — strftime directives produce literal text, not a formatted date
t.Format("%Y-%m-%d")            // => "%Y-%m-%d" with the % treated as literal runes

// RIGHT — the reference date is the layout
t.Format("2006-01-02")          // => "2026-06-25"
t.Format("2006-01-02 15:04:05") // => "2026-06-25 14:30:05"

Prefer the named constants over hand-typed magic numbers when one fits: time.DateOnly is "2006-01-02", time.DateTime is "2006-01-02 15:04:05", time.TimeOnly is "15:04:05", time.RFC3339 is "2006-01-02T15:04:05Z07:00" (time constants). For a zero-padded numeric field use 01/02/15; 1/2/3 mean no padding; Jan/Mon are the textual month/day. Verified: Format("2006-01-02") of 25 June 2026 yields "2026-06-25".


3. time.Duration Is an int64 of Nanoseconds

"A Duration represents the elapsed time between two instants as an int64 nanosecond count" (time). The trap: a Duration is just an integer, so a bare literal is nanoseconds, not seconds. time.Sleep(1) sleeps one nanosecond; time.Sleep(5) sleeps five nanoseconds. You almost never want that — multiply by a unit constant.

// WRONG — these are nanoseconds; this sleeps ~instantly, not for 5 seconds
time.Sleep(5)
ctx, cancel := context.WithTimeout(ctx, 30) // 30 nanoseconds

// RIGHT — typed literals: multiply by the unit
time.Sleep(5 * time.Second)
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
d := 100 * time.Millisecond

The unit constants (time.Nanosecondtime.Hour) are themselves Duration values; time.Second is 1_000_000_000. Verified: time.Duration(1) != time.Second, and time.Duration(1) == 1*time.Nanosecond. To turn a number from outside (a config field, a flag) into a duration, multiply (time.Duration(n) * time.Second) — and prefer parsing a string with time.ParseDuration("30s") so the unit is explicit on the wire.


4. Measure Elapsed Time With time.Since (Monotonic), Not Wall Subtraction

Elapsed time must come from the monotonic clock, which only moves forward and is immune to NTP corrections and DST jumps. time.Since(start) "is shorthand for time.Now().Sub(t)" (time) and uses the monotonic reading when start came from time.Now().

// WRONG — re-derives wall times and subtracts; an NTP step or DST change can
// make this negative or wildly off, and it discards the monotonic reading
start := time.Now()
do()
elapsed := time.Now().Sub(start.Round(0)) // Round(0) just stripped the monotonic reading

// RIGHT — Since uses the monotonic clock; non-negative, jump-immune
start := time.Now()
do()
elapsed := time.Since(start)

Verified: time.Since(start) is always >= 0. Beware operations that strip the monotonic reading: "t.AddDate(y, m, d), t.Round(d), and t.Truncate(d) are wall time computations, they always strip any monotonic clock reading"; so do t.In, t.Local, t.UTC (time, Monotonic Clocks). If you round or change zone, you lose the monotonic reading for later elapsed math. For tests, prefer testing/synctest (Go 1.25) over real sleeps — owned by go-testing-advanced.


5. Stop Tickers; Don't Leak time.After in a Hot Loop

A time.Ticker must be stopped — it is the one timer the GC change below does not fully cover for correctness, and a forgotten Stop() keeps it firing.

// RIGHT — always stop a ticker you create
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-ticker.C:
		poll()
	}
}

time.After is fine for a one-shot timeout, but in a select inside a hot loop it allocates a fresh timer on every iteration that lives until it fires:

// WRONG — a new timer per iteration; under a fast inner channel these pile up
for {
	select {
	case v := <-work:
		handle(v)
	case <-time.After(time.Minute): // fresh timer each loop; not reused
		return errTimeout
	}
}

// RIGHT — one context deadline, or one reusable NewTimer, outside the loop
timer := time.NewTimer(time.Minute)
defer timer.Stop()
for {
	select {
	case v := <-work:
		handle(v)
		if !timer.Stop() {
			<-timer.C
		}
		timer.Reset(time.Minute)
	case <-timer.C:
		return errTimeout
	}
}

Go 1.23 changed timer GC and channel buffering (gated on the module's go directive being 1.23.0+): "Timers and Tickers that are no longer referred to by the program become eligible for garbage collection immediately, even if their Stop methods have not been called"; and "the timer channel associated with a Timer or Ticker is now unbuffered, with capacity 0 ... no stale values prepared before [a Reset/Stop] call will be sent or received after the call" (Go 1.23). So on 1.23+ the old if !t.Stop() { <-t.C } drain dance is no longer needed to avoid a stale tick, and an un-stopped one-shot timer no longer leaks memory. Still call Stop() on a ticker for correctness (to stop the work), and prefer a context deadline for request timeouts — see go-context. The version floor is owned by go-version-feature-map.


6. time.Parse Returns an Error — Check It

time.Parse(layout, value) returns (Time, error). A mismatched layout, an out-of-range field, or a bad zone is reported in that error, not by a panic and not by a zero Time you can detect after the fact. Discarding it is the policy-root violation (go-idiomatic-discipline §4).

// WRONG — a bad input becomes the zero Time (year 1, UTC), silently
t, _ := time.Parse(time.RFC3339, raw)

// RIGHT — check, wrap with context, return
t, err := time.Parse(time.RFC3339, raw)
if err != nil {
	return time.Time{}, fmt.Errorf("parsing timestamp %q: %w", raw, err)
}

Verified: parsing a value whose shape does not match the layout returns a non-nil error; a matching layout round-trips (Parse then Format reproduces the input). Use time.ParseInLocation when the input has no zone and you need it interpreted in a specific Location rather than UTC. The %w wrapping and errors.Is/As belong to go-error-handling.


7. Zones: time.UTC, time.Local, and LoadLocation

Store and transmit instants in UTC; convert to a named zone only for display. time.UTC and time.Local are always available; any other zone comes from time.LoadLocation("America/New_York"), which reads the system (or embedded time/tzdata) database and returns an error you must check.

loc, err := time.LoadLocation("America/New_York")
if err != nil {
	return fmt.Errorf("loading zone: %w", err)
}
local := t.UTC().In(loc) // In() strips the monotonic reading (§4) — display only

t.In(loc), t.Local(), and t.UTC() change only the interpretation for display; they do not change the instant — but they strip the monotonic reading, so do elapsed-time math before converting. A zero time.Time{} is 0001-01-01 00:00:00 UTC, not "now" and not "unset"; for JSON, drop a zero time with omitzero (Go 1.24), not omitempty — owned by go-json.


8. Routing to Related Skills

  • go-idiomatic-discipline — the policy root; §6's checked time.Parse error is its "errors are values" floor.
  • go-error-handling — wrapping the Parse/LoadLocation error (%w, errors.Is/As).
  • go-json — the zero time.Time that omitzero (1.24) drops where omitempty leaks "0001-01-01T00:00:00Z".
  • go-contextcontext.WithTimeout/WithDeadline over ad-hoc timers for request cancellation (§5).
  • go-concurrency-goroutines — a ticker driven in a goroutine loop, stopped via ctx.Done().
  • go-testing-advancedtesting/synctest (1.25) for deterministic time in tests instead of real sleeps.
  • go-version-feature-map — version floors: the 1.23 timer/ticker GC + channel change, omitzero (1.24).

9. Reference Files

High-frequency time anti-patterns in LLM-generated Go, each with wrong/right code and citations:

${CLAUDE_SKILL_DIR}/references/common-mistakes.md

Source provenance for every claim in this skill:

${CLAUDE_SKILL_DIR}/references/sources.yaml

What ships with it: 2 files

12.2 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,452. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.