The snapshot, not the stream
You are building a rate limiter. You need to know exactly when a request arrived to decide if the user is flooding the API. Or you are writing a log file and need a timestamp that doesn't drift when the system clock jumps. Getting the current time sounds trivial until you realize you need it in a specific timezone, formatted for a database, or compared against a deadline without losing precision.
Go's time package treats time as a value, not a resource. time.Now() returns a time.Time struct that captures an instant. Copying the struct copies the instant. There is no shared state, no locking, no race condition on the timestamp itself. You can pass a time.Time to any goroutine and it remains the same moment forever.
Think of time.Time as a coordinate on a timeline. It is not a live feed that updates itself. When you call time.Now(), you grab a snapshot of the current instant. That snapshot is a value type. Copying it doesn't create a second clock. It just copies the coordinate. This matters because you can pass timestamps around without worrying about them changing underneath you.
How time.Time works
When you call time.Now(), the runtime queries the operating system for the current wall-clock time. It returns a time.Time struct containing the number of nanoseconds since the Unix epoch and a pointer to a time.Location. The location determines how that instant maps to human-readable hours and minutes.
The struct also carries a monotonic clock reading. This is an internal counter that only moves forward and never jumps. When you subtract two time.Time values, Go uses the monotonic clock to calculate the duration. This makes duration measurements immune to system clock adjustments, NTP corrections, or daylight saving time transitions.
Pass time.Time by value. The struct is small enough that copying it is cheaper than allocating a pointer. You don't need *time.Time unless you need to represent a nil timestamp. Even then, the zero value of time.Time is a valid instant (January 1, year 1), so use IsZero() to check for unset times rather than relying on nil pointers.
Minimal example
Here's the baseline: capture the time and format it using Go's unique reference-time layout.
package main
import (
"fmt"
"time"
)
func main() {
// Capture the current instant as a value.
// The struct holds nanoseconds since epoch and a location pointer.
now := time.Now()
// Format uses a reference time, not placeholders.
// 2006-01-02 15:04:05 matches the layout Mon Jan 2 15:04:05 MST 2006.
layout := "2006-01-02 15:04:05"
formatted := now.Format(layout)
fmt.Println(formatted)
}
The reference time layout
Go doesn't use a dictionary of codes like %Y or %d. It compares your layout string against the fixed reference time Mon Jan 2 15:04:05 MST 2006. If you write 2006 in your layout, Go outputs the year. If you write 01, it outputs the month. The position and value in the reference time dictate the output.
This design forces you to see the structure of the format string. You can't accidentally swap month and day because the reference time makes the order explicit. 01 is always month. 02 is always day. 15 is always hour. 04 is always minute. 05 is always second. MST outputs the timezone abbreviation. Mon outputs the weekday.
If you forget the reference time, the output will be garbage. Writing YYYY-MM-DD produces the literal string YYYY-MM-DD because none of those characters match the reference time. The compiler won't catch this. It's a runtime formatting error.
Memorize the reference time. It's the key to every format string.
The monotonic clock
Here's a detail that saves you from subtle bugs. time.Now() returns a time with a monotonic clock component. When you call Sub or After, Go uses the monotonic clock if both times have one. This means duration calculations are precise even if the system clock jumps backward or forward.
package main
import (
"fmt"
"time"
)
func main() {
// start captures the monotonic reading along with the wall clock.
start := time.Now()
// Simulate work.
time.Sleep(100 * time.Millisecond)
// end captures a new monotonic reading.
end := time.Now()
// Sub uses the monotonic clock to compute duration.
// This result is safe even if the system clock changed during sleep.
elapsed := end.Sub(start)
fmt.Printf("Elapsed: %v\n", elapsed)
}
If you strip the monotonic clock by calling UTC() or In(), the resulting time loses the monotonic component. Subtracting two times that lack monotonic readings falls back to wall-clock arithmetic, which can be inaccurate if the clock adjusted between the two calls. Keep monotonic times for duration math. Convert to UTC or a location only when you need to display or store the instant.
Use subtraction for durations. Use time.Now() for instants.
Timezones are rules
Timezones are not offsets. An offset like -05:00 is a snapshot of a rule at a specific moment. Rules change. Daylight saving time starts and ends. Countries shift their offsets. A time.Location holds the IANA rules for a zone, including all historical and future transitions.
When you call In(loc), Go applies the rules to find the correct offset for that instant. This means America/New_York automatically switches between EST and EDT. Hardcoding an offset fails when DST starts. Always use named locations for display.
time.LoadLocation parses the IANA timezone database. It returns an error if the zone name is invalid. The error check is verbose. The community accepts the boilerplate because it makes the failure path visible. If you ignore the error and use a nil location, In(nil) panics with a nil pointer dereference.
Realistic example
Here's how you handle timezones in a function, loading a location and converting an instant without mutating the source.
package main
import (
"fmt"
"time"
)
// formatTimeForUser converts an instant to a user's timezone and formats it.
func formatTimeForUser(t time.Time, zone string) string {
// Load the timezone data from the system or Go's internal zoneinfo.
loc, err := time.LoadLocation(zone)
if err != nil {
// Return a safe fallback if the zone is unknown.
// In production, log the error and return a default format.
return t.Format(time.RFC3339)
}
// In returns a new Time value.
// The original time t remains unchanged.
localTime := t.In(loc)
// Format for display, including the timezone abbreviation.
// MST in the layout outputs the zone abbreviation like EST or EDT.
return localTime.Format("2006-01-02 15:04:05 MST")
}
func main() {
now := time.Now()
fmt.Println(formatTimeForUser(now, "America/New_York"))
fmt.Println(formatTimeForUser(now, "Europe/London"))
}
Context often carries deadlines derived from time.Time. context.WithDeadline takes a time.Time to set an absolute cutoff. context.WithTimeout takes a time.Duration to set a relative cutoff. Functions that accept a context should respect cancellation and deadlines. The context always goes as the first parameter, conventionally named ctx.
Store UTC. Display local. Never store local time in a database.
Pitfalls and errors
If you forget to import the package, the compiler rejects the file with undefined: time. If you try to use a time.Time where a string is expected, you get cannot use t (variable of struct type time.Time) as string value in argument.
time.Parse assumes UTC if the layout doesn't specify a timezone. This is a common trap. If you parse a string like 2023-10-27 14:30:00 with layout 2006-01-02 15:04:05, the result is in UTC. If you intended local time, you must use time.ParseInLocation or include MST in the layout.
The compiler complains with parsing time "2023-10-27" as "2006-01-02 15:04:05": unexpected end of input if the input string is shorter than the layout. It also rejects mismatched formats with parsing time "2023-10-27" as "2006-01-02": cannot parse " 14:30:00" as "".
The zero value of time.Time is January 1, year 1, 00:00:00 UTC. Printing a zero time outputs 0001-01-01 00:00:00 +0000 UTC. Use IsZero() to check for unset times. Don't compare against time.Time{} directly unless you understand the implications of the zero instant.
time.LoadLocation can return an error if the zone name is misspelled or the system lacks timezone data. Always handle the error. Discarding it with _ hides configuration bugs until runtime.
Trust gofmt. The code blocks follow standard formatting. Argue logic, not indentation.
Decision matrix
Use time.Now() when you need the current instant in the system's local timezone for logging or debugging.
Use time.Now().UTC() when you store timestamps in a database or send them over the network to avoid daylight saving time ambiguity.
Use time.Now().In(loc) when you must display the time in a specific user timezone like America/New_York.
Use time.Date(year, month, day, hour, min, sec, nsec, loc) when you construct a specific instant from components rather than reading the clock.
Use time.Since(t) when you measure elapsed duration from a past instant to now.
Use time.Until(t) when you calculate the duration until a future deadline.
Use time.Parse(layout, value) when you convert a string to a time and the input is guaranteed to be in UTC or includes timezone information.
Use time.ParseInLocation(layout, value, loc) when you parse a string that lacks timezone data and you know the intended location.