Skip to content
academia.sh

Lesson 09 / 12

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.

Published

In this lesson

  • 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()
Contents

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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close