# Just About Go Time

> **Source:** [http://localhost:1313/just-about-go-time/](http://localhost:1313/just-about-go-time/)
> **Author:** [Vikash Patel](https://vikashpatel.net)
> **Published:** May 10, 2026
> **Reading Time:** 6 min
> 
> *This is the raw Markdown source of the article from the [Lorbic Technical Journal](http://localhost:1313/).*

---


Time is an illusion. Or more accurately, time is a political consensus poorly masquerading as physics. 

![Time is relative, absolute, and a scam](time-theory-meme.jpg)

If you've ever seen Dylan Beattie's "Plain Text" [talk](https://www.youtube.com/watch?v=gd5uJ7Nlvvo), you know that humans have spent centuries making data storage as complicated as possible. But text encoding has nothing on time zones.

As engineers, we like to pretend that `time.Now()` returns an objective truth. It doesn't. It returns a snapshot of a highly contested, historically unstable set of political boundaries. In 2011, the island nation of Samoa decided they wanted to align their workweek with Australia rather than the United States. To do this, they didn't just change their clocks; they completely skipped Friday, December 30th. At 11:59 PM on Thursday, the clock ticked over, and it was suddenly Saturday.

A whole day, erased from existence because a prime minister signed a piece of paper. If your backend job ran a cron on December 30th in Samoa, it just... didn't happen.

Here is what I've learned about handling time without losing sleep over it.

## Go Time Package Mechanics

Go's `time` package is notorious for being incredibly robust, but deeply weird to newcomers.

### The 1 2 3 4 5 6 7 Reference Date

If you want to parse or format a date in Python or Java, you use a format string like `YYYY-MM-DD HH:mm:ss`. 

Go looked at that and said, "No, that's too abstract". Instead, Go uses a reference date. To format a date, you have to write out what the reference date would look like in your desired format.

The reference date is: **January 2nd, 3:04:05 PM, 2006, timezone -0700 (MST)**.

Why? Because if you write it out in the American format, it counts up sequentially:
`01/02 03:04:05 '06 -0700`

It is a dry engineering joke that has cursed millions of developers to Google "golang time format string" every single day of their careers.

```go
// Parsing an ISO 8601 string
t, err := time.Parse("2006-01-02T15:04:05Z07:00", "2026-05-15T14:30:00Z")

// Formatting to a custom layout
fmt.Println(t.Format("2006-01-02 15:04")) 
```

### Monotonic Clocks in Go

Before Go 1.9, measuring execution time was dangerous.

```go
start := time.Now()
doWork()
elapsed := time.Since(start)
```

In older versions of Go, `elapsed` could theoretically be a negative number. 

Why? Because `time.Now()` used the operating system's "wall clock". The wall clock is tied to NTP (Network Time Protocol). If the OS synched with an NTP server during `doWork()` and realized its clock was 2 seconds fast, it would violently jump the clock backward. Your start time would suddenly be in the future.

In 2017, Russ Cox proposed a transparent fix. Rather than creating a separate `MonotonicTime` type, Go simply jammed a monotonic clock reading inside the existing `time.Time` struct alongside the wall clock. 

If a `time.Time` struct has a monotonic clock reading, operations like `time.Since()` or `t.Sub(u)` automatically use the monotonic (always forward-moving) clock to measure duration, completely ignoring any NTP timezone jumps. It fixed a massive class of distributed system bugs without breaking a single line of backward compatibility.

### Testing Temporal Logic

Writing tests for code that depends on the current time is a common source of flaky CI/CD pipelines. If your logic checks if a token is expired using `time.Now()`, your test is coupled to the execution clock of the runner.

To solve this, treat time as a dependency. Never call `time.Now()` inside deep business logic. Instead, pass the time in as an argument or use a provider function.

```go
type Now func() time.Time

func IsExpired(expiry time.Time, now Now) bool {
    return now().After(expiry)
}
```

By passing a `Now` function, you can inject a fixed timestamp in your tests, ensuring your logic is deterministic and reproducible.

## Database Persistence Patterns

How you store time depends on your database. Here is what works.

### PostgreSQL timestamptz Storage

PostgreSQL provides the `TIMESTAMP WITH TIME ZONE` (or `timestamptz`) type. It is a common misconception that this type stores the timezone. 

When you send a timestamp with an offset to a `timestamptz` column, PostgreSQL performs a three-step operation:
1. It parses the incoming string and offset.
2. It converts the value to UTC.
3. It stores the UTC value as an 8-byte integer.

The offset is discarded. This conversion happens on the fly. While it is a session-level CPU cost rather than a storage bottleneck, it causes panic when a developer queries the database from a GUI tool set to local time, sees a different hour than what is on the server, and assumes the database is corrupted. The data is fine. Your DB tool is just "helping" you.

```sql
-- Checking the current session timezone
SHOW timezone;

-- Retrieving UTC data formatted for a specific zone
SELECT created_at AT TIME ZONE 'America/New_York' FROM transactions;
```

### Couchbase and NoSQL UTCMilli Persistence

In a NoSQL JSON document store like Couchbase, you should prioritize **Millisecond Unix Epochs (UTCMilli)** for database storage.

```json
{
  "created_at": 1778857200000
}
```

Storing time as a 64-bit integer is the most accurate way to persist temporal data. It is inherently normalized to UTC, avoids the overhead of string parsing, and prevents timezone ambiguity. Because it is a numeric type, it supports high-performance range scans and sorting in SQL++ without the need for complex casting.

Treat ISO 8601 strings as a serialization format for transit, not storage. If you need human readability during a 3 AM incident response, use the `MILLIS_TO_STR()` function in your query. The minor benefit of raw readability in a JSON file is never worth the loss of numeric precision.

## The Handshake Boundary

While the database wants integers, the network wants strings. The absolute rule for the client-server boundary is a two-step handshake:

1. **Transport:** The backend sends an **ISO 8601 String** ending in "Z".
2. **Presentation:** The client converts that UTC string into **Local Time**.

Never perform localization on the server. A mobile device can change timezones while a request is in flight. The backend should provide the raw UTC data, and the client application must use the local operating system APIs to format the string for the user.

### Browser JavaScript Formatting

JavaScript's `Date` object is notoriously flawed, but modern browsers have finally stabilized `Intl.DateTimeFormat`. You pass it the UTC string, and the browser automatically detects the user's OS timezone and formats it correctly.

```javascript
const utcString = "2026-05-15T14:30:00Z";
const date = new Date(utcString);

const formatter = new Intl.DateTimeFormat('default', {
  year: 'numeric', month: 'short', day: 'numeric',
  hour: 'numeric', minute: '2-digit'
});

console.log(formatter.format(date)); 
```

### Mobile OS Localization

Mobile OS libraries are specifically designed to handle timezone changes on the fly. 

On iOS (Swift), parse the string using `ISO8601DateFormatter` and format it with `DateFormatter`. The OS will automatically pick up the user's current locale and timezone.

```swift
let formatter = ISO8601DateFormatter()
let date = formatter.date(from: "2026-05-15T15:00:00Z")!

let localFormatter = DateFormatter()
localFormatter.dateStyle = .medium
localFormatter.timeStyle = .short
// Let the iPhone figure out if they just flew to Samoa
print(localFormatter.string(from: date)) 
```

Android (Kotlin) uses the `java.time` package (`Instant`). You parse the UTC moment and convert it to the system's default zone for display.

```kotlin
val instant = Instant.parse("2026-05-15T15:00:00Z")
val localDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault())

val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
println(localDateTime.format(formatter))
```

These libraries are updated by the OS vendors whenever political boundaries or DST rules change, shielding your application from maintenance overhead.

## The short version

Measure duration with monotonic clocks. Store time as UTCMilli integers in NoSQL, `timestamptz` in Postgres. Send ISO 8601 strings over the wire. Let the client handle formatting.

Do that consistently and your system stays correct even when governments decide to delete a Friday.

## Further Reading
- [The Amazing "Plain Text" Talk by Dylan Beattie](https://www.youtube.com/watch?v=gd5uJ7Nlvvo)
- [Go Official: Time Package Documentation](https://pkg.go.dev/time)
- [Russ Cox: Monotonic Elapsed Time Measurements in Go](https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md)
- [PostgreSQL Official: Date/Time Types (timestamptz)](https://www.postgresql.org/docs/current/datatype-datetime.html)
- [Couchbase Official: SQL++ Date Functions](https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/datefun.html)

