---
title: 'Time Operations'
source: 'https://academia.sh/en/courses/go-applications/time-operations'
course: 'The Standard Library and Network Applications'
language: en
updated: '2026-09-14T10:59:31+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures that a moment is preserved through Equal when it turns into text and comes back, but the time zone's name does not carry across"
    - 'Shows that time.Duration is a value carrying its own type and is distinct from a plain number'
    - 'Limits by running that two values representing the same moment come out equal with Equal but different with == and String()'
---

# Time Operations

A moment turning into text and coming back is measured: the moment itself is preserved through Equal, but even though the numeric time zone offset is rebuilt, the zone's name does not carry across. time.Duration is shown to be a value carried by its own type, not a plain number. The limiting measurement runs two values representing the same moment to show that they come out equal with Equal while == and String() show them as different.

The previous lesson showed that a link can be built at compile time instead of run time: a
source file's content froze together with the program itself. Time offers exactly the opposite
example — no value is as tied to the run itself as a **moment**. A moment is defined by the very
instant it is measured; writing it to text and reading it back leads to a different kind of loss
than the ones measured in the previous two lessons.

This difference has a concrete consequence: in the file and buffer lesson, the value crossing
the boundary never carried type information at all — the loss was complete every time. With a
time value the situation is more subtle: the text crossing the boundary carries some information
and not other information, and which one falls into which category depends on the layout used
for writing it. This lesson puts three separate values (a moment, a time zone, a duration)
through the same "turn into text and come back" test and measures separately how much of each
survives.

**Determinism note:** the real clock is never read in this lesson (`time.Now` is not called
anywhere). Every example rests on a single moment parsed from a fixed text; the comparisons are
also built on top of this fixed moment.

## Starting from a Fixed Moment

The type representing a moment in Go's standard library is `time.Time`; `time.Parse` reads a
string according to a given **layout** and produces a `time.Time` value. The layout used in this
lesson is RFC 3339 — a time format common in network and file formats that also carries a time
zone.

```go
// fixed-moment.go — the real clock is not read, the moment is parsed from a fixed text
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, err := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	if err != nil {
		panic(err)
	}
	fmt.Printf("parsed value type=%T\n", fixedMoment)
	fmt.Println("time zone (Z = UTC)  :", fixedMoment.Location())
}
```

```
parsed value type=time.Time
time zone (Z = UTC)  : UTC
```

The `Z` at the end of the string means "UTC, zero offset" in the RFC 3339 layout; `time.Parse`
recognizes this and binds the result directly to the `time.UTC` location. The only "real" data
used throughout this lesson is this one fixed string; every step after it is derived from this
value. `time.Parse`'s second argument is the text to parse, and its first argument is the
expected **layout** — not the text itself, but an example of the text's format. This example is
not just a placeholder, it is a real date: the `time.RFC3339` constant is defined in Go's
standard library as `2006-01-02T15:04:05Z07:00`, and this specific date (the digits corresponding
to month/day/hour/minute/second/year in Go's own internal ordering) is the standard way of
specifying a layout — a different approach from another language's abstract placeholders like
`YYYY-MM-DD`.

A `time.Time` value does not always carry the same kind of data. Only values coming from a
`time.Now()` call carry a **monotonic** reading used for duration measurements (separate from
the wall clock, a count that only moves forward); values derived from text through something
like `time.Parse` never carry this extra reading, they are only a plain wall-clock value. Because
this lesson only uses `time.Parse`, every value measured is of this second, "plain" kind.

## When Time Turns into Text and Comes Back

**SD8.** — The fixed moment is turned into text with `Format` and parsed back with the same
layout. This is the crossing that also appears as one line in the Networking and Data Formats
topic's shared core (a time value to text and back: rebuilt); that topic's binding 1/5/7
distribution is referenced here in a single sentence, the core is not rebuilt. This lesson
verifies that line and extends it with a detail specific to time zones.

```go
// back-to-text.go — a moment is written to text and read back with the same layout
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")

	text := fixedMoment.Format(time.RFC3339)
	back, _ := time.Parse(time.RFC3339, text)

	fmt.Println("written back to text :", text)
	fmt.Println("same moment (Equal)  :", back.Equal(fixedMoment))
	fmt.Printf("returned type        : %T\n", back)
}
```

```
written back to text : 2026-08-23T10:00:00Z
same moment (Equal)  : true
returned type        : time.Time
```

When a `time.Time` value is written to a string, the type information is lost completely — the
result is a plain string, with no marker inside it saying "this is a time." But the code calling
`time.Parse` already knows the string in its hand is a time, and rebuilds it by supplying the
right layout; the information that slipped away is **rebuilt** with this knowledge on the
caller's side. The `Equal` result is `true`: the rebuilt moment is exactly the same as the
original moment. On the topic's measurement axis, this line falls into the "rebuilt" class —
just like a struct tag being rebuilt from JSON, what makes the rebuild possible here is not the
string itself, but the layout the caller already knows.

## The Time Zone's Name Does Not Carry Across

The measurement above hides one detail: the fixed moment is exactly at UTC, that is, at zero
offset. Because the RFC 3339 layout marks zero offset specially with the letter `Z`, the rebuilt
value also binds exactly to `time.UTC`. The picture changes when an offset other than zero is
involved.

**SD9.** — The fixed moment is converted to a time zone named `ZoneA` (not a real geographic
region, just a **fictional** time zone with a +03:00 offset), written to text and read back. The
measurement compares both the numeric offset and the zone name separately.

```go
// zone-name.go — the numeric offset is preserved, the zone's name is not carried across
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	zoneA := time.FixedZone("ZoneA", 3*3600)
	local := fixedMoment.In(zoneA)

	text := local.Format(time.RFC3339)
	back, _ := time.Parse(time.RFC3339, text)

	localName, localOffset := local.Zone()
	backName, backOffset := back.Zone()

	fmt.Println("written back to text   :", text)
	fmt.Println("same moment (Equal)    :", local.Equal(back))
	fmt.Println("offset preserved       :", localOffset == backOffset)
	fmt.Println("zone name carried over :", localName == backName)
}
```

```
written back to text   : 2026-08-23T13:00:00+03:00
same moment (Equal)    : true
offset preserved       : true
zone name carried over : false
```

The RFC 3339 layout never carries a time zone's name, it only writes its numeric offset
(`+03:00`). When read back, `time.Parse` rebuilds this offset correctly — the moment is still the
same, `Equal` is still `true`, the offset comparison is also `true`. But the zone name
(`"ZoneA"`) never comes back, because it is never written to the text anywhere; the name of the
new location `Parse` produces comes out different from the original `"ZoneA"`. This reads the
same way as the previous lesson: the numeric offset is **rebuilt**, because the text itself
carries it; the zone name is **lost**, because the text itself never carries it at all. The same
crossing falls into two different classes depending on which part of the information is being
carried.

## time.Duration Is a Type, Not a Number

A duration itself also carries a separate type in Go: `time.Duration`, a duration in nanoseconds
that stores an `int64` underneath but is a type in its own right. Unlike a plain number, its
difference is that it carries its own formatting (like `1h30m0s`), and mixing it up with another
kind of number is rejected by the compiler.

**SD10.** — A duration string (`"1h30m"`) is parsed and its type and formatted text are
measured; then whether the same value is equal to a raw integer equivalent, and how the
formatting changes when it is doubled, are run.

```go
// duration-type.go — time.Duration carries its own type, separate from a raw number
package main

import (
	"fmt"
	"time"
)

func main() {
	duration, _ := time.ParseDuration("1h30m")
	fmt.Printf("duration type=%T value=%s\n", duration, duration)
	fmt.Println("equal to int64 equivalent:", int64(duration) == 5400_000_000_000)

	doubled := duration * 2
	fmt.Println("doubled:", doubled)
	fmt.Println("in hours:", doubled.Hours())
}
```

```
duration type=time.Duration value=1h30m0s
equal to int64 equivalent: true
doubled: 3h0m0s
in hours: 3
```

The data underlying the `duration` variable is an `int64` — a count of nanoseconds — but `%T`
shows it not as a raw integer but as `time.Duration`, and when `fmt.Println` prints it directly,
the result is `1h30m0s`, not `5400000000000`. This is a distinction the compiler builds between
an integer and a duration: there is no implicit conversion between the `int64` underlying
`duration` and another `int64` variable; the two are separate types. The multiplication
`doubled := duration * 2` preserves the duration type, and its formatting updates automatically
too — `3h0m0s`, which also gives `3` when queried with the `Hours()` method.

A duration, just like a moment, can also turn into text and come back — and this crossing turns
out less lossy than a moment's crossing.

**SD11.** — The `duration` variable is turned into text with `String()`, and the same text is
read back with `ParseDuration`; a syntactically broken text is also tried.

```go
// duration-to-text.go — a duration turns into text and comes back, an invalid text is rejected
package main

import (
	"fmt"
	"time"
)

func main() {
	duration, _ := time.ParseDuration("1h30m")
	text := duration.String()
	back, backErr := time.ParseDuration(text)
	fmt.Println("duration to text           :", text)
	fmt.Println("same value back            :", back == duration)
	fmt.Println("read back without error    :", backErr == nil)

	_, invalidErr := time.ParseDuration("1h30mx")
	fmt.Println("invalid text gave an error :", invalidErr != nil)
}
```

```
duration to text           : 1h30m0s
same value back            : true
read back without error    : true
invalid text gave an error : true
```

Unlike a moment turning into text, writing a `time.Duration` loses no information at all: the
`back == duration` comparison is `true`, because the text `String()` produces (`1h30m0s`) carries
every unit making up the duration (hours, minutes, seconds) exactly. A duration has no "name" or
"context" — the only information it carries is its own length, and this information fits into
the text exactly. This is a complete contrast with the previous measurement, which could not
carry the time zone's name: whether there is a loss depends on how much information the value
itself carries, not on the writing format used. The last line adds a separate observation: a
syntactically broken duration text does not silently fall to `0`; `ParseDuration` rejects it with
an error — the duration goes through a check here too, not at compile time but at least at parse
time.

## Limiting Measurement: One Moment, Two Representations

The measurements so far examined a moment in a single time zone. The step below represents the
same moment in two different fictional zones and runs three separate comparison tools (`Equal`,
`==`, `String`) side by side.

**SD12.** — Two fictional zones (`ZoneA` +03:00, `ZoneB` −05:00) are applied to the same fixed
moment. All three give a different answer to the "are they the same" question.

```go
// two-representations.go — the same moment in two zones, measured with three comparison tools
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	zoneA := time.FixedZone("ZoneA", 3*3600)
	zoneB := time.FixedZone("ZoneB", -5*3600)

	viewA := fixedMoment.In(zoneA)
	viewB := fixedMoment.In(zoneB)

	fmt.Println("ZoneA local time   :", viewA.Format("15:04"))
	fmt.Println("ZoneB local time   :", viewB.Format("15:04"))
	fmt.Println("same with Equal    :", viewA.Equal(viewB))
	fmt.Println("same with ==       :", viewA == viewB)
	fmt.Println("same with String() :", viewA.String() == viewB.String())
}
```

```
ZoneA local time   : 13:00
ZoneB local time   : 05:00
same with Equal    : true
same with ==       : false
same with String() : false
```

The three comparison tools answer three different questions. `Equal` compares the moment the two
values represent — since both belong to the same moment, the result is `true`. The `==` operator
instead compares `time.Time`'s internal structure exactly; because this structure also contains
the time zone information, two values bound to two different zones do not come out equal with
`==`. `String()` differs for the same reason: each one writes its own local time and offset, one
"13:00 +03:00" and the other "05:00 -05:00" — they do not overlap at all at the text level.

The difference among these three is a trap Go's documentation explicitly warns about: the `==`
operator is a comparison that should not be used for `time.Time` values, because code using it
does not answer the question "is it the same moment," but "is it the same representation" — most
of the time the first question is what is wanted, but the operator itself answers the second. On
the topic's measurement axis, this draws a boundary specific to this lesson: even when two values
represent the same moment, the comparison tool itself determines which question is being asked;
the word "same" alone does not say which comparison is meant.

The same distinction holds for ordering too. The `Before` and `After` methods, just like `Equal`,
look at the moment, not the representation; they correctly say which one comes earlier in time
even when the two values are written in different zones.

```go
// ordering.go — Before and After also look at the moment, not the representation
package main

import (
	"fmt"
	"time"
)

func main() {
	fixedMoment, _ := time.Parse(time.RFC3339, "2026-08-23T10:00:00Z")
	oneHourLater := fixedMoment.Add(time.Hour)

	zoneA := time.FixedZone("ZoneA", 3*3600)
	viewA := fixedMoment.In(zoneA)
	laterViewB := oneHourLater.In(time.FixedZone("ZoneB", -5*3600))

	fmt.Println("is viewA before laterViewB:", viewA.Before(laterViewB))
	fmt.Println("is laterViewB after viewA :", laterViewB.After(viewA))
}
```

```
is viewA before laterViewB: true
is laterViewB after viewA : true
```

Even though the two values are written in completely different zones and completely different
local times, `Before` and `After` find the correct order, because both of them make the
comparison through the represented moment — they do not look for an exact match of the internal
structure the way `==` does. This confirms the previous paragraph's general rule: when comparing
`time.Time` values, `Equal`, `Before` and `After` form a safe foundation, while `==` is safe only
when the two values are known to have been produced in the same zone, in the same way.

## Summary

- A moment is preserved through `Equal` when it turns into text and is read back with the same
  layout; this is the verified version of the "rebuilt" line in the topic's shared core.
- The RFC 3339 layout carries a time zone's numeric offset but not its name: the rebuilt value's
  offset matches the original while the zone name does not.
- `time.Duration` keeps its own type even though it carries an `int64` underneath; its
  formatting and arithmetic operate according to this type, and it is not mixed up with a raw
  number. Turning into text and coming back also loses no information, because the only thing it
  carries is its own length.
- Limiting measurement: two values representing the same moment come out equal with `Equal` but
  different with `==` and `String()` — the three tools answer three different questions.
- `Before` and `After` also look at the moment, like `Equal`, not the representation; the `==`
  operator is a safe comparison only when the two values are known to have been produced in the
  same zone and format.

## Next Step

This lesson showed that a time value carries no field names as it turns into text, it only
produces a raw representation — the text `2026-08-23T13:00:00+03:00` carries no marker saying
which field holds which value, it can only be inferred from position. A reader can interpret
this text correctly because it already knows the RFC 3339 layout — just like the embed directive
in the previous lesson placing content into a variable that already knows the target type. But
not every format is this generous: a field inferred from position can no longer be inferred once
the position is lost or the format changes.

The next lesson measures a format that fills exactly this gap: does carrying field names too,
while writing an event to a log, make the same record parseable for both a human and another
program? This lesson's fixed timestamp will be used exactly the same way in the next lesson's
log records — time will again come from a fixed text, not the real clock.
