JavaRush /Courses /Go SELF /Layout formatting and time parsing in Go

Layout formatting and time parsing in Go

Go SELF
Level 34, Lesson 2
Available

1. Layout in Go: the idea and a cheat sheet

When you start writing “real” programs, time suddenly crawls out from everywhere. You need to show the user “when it was created,” accept input like “deadline by Friday,” output a report like “completed 3 tasks per day.” At the same time, time.Time is a structured value, while the user sees and enters strings, so we need a bridge between them.

And this is where it’s easy to make two mistakes. The first is printing the time “however it comes out” (fmt.Println(t)) and then complaining that the format is ugly and changes depending on locale/zone. The second is trying to parse a date “by eyeballing it” with Split and manual Atoi. You can, of course, but it’s like fixing a bike with your teeth: technically possible, but why.

In Go, there’s a standard path for this: the Format method on time.Time and the time.Parse/time.ParseInLocation functions.

What the “magic layout” is and why it looks weird

Go formats time not via templates like YYYY-MM-DD, as many languages do, but via a so-called layout—a sample string where you write a specific reference date. It looks like a spell: 2006-01-02 15:04:05. The first time you see it, your brain tries to close the tab and run into the woods.

The idea is simple: a layout is an “example of what the time should look like.” Go takes your sample string and replaces the elements of the reference date with the corresponding parts of the real time. That is, you don’t say “year with two digits,” you say “here is where the year goes”—and you show the year on the reference date.

Why is the reference date exactly that? Because Mon Jan 2 15:04:05 MST 2006 is a convenient set of numbers where almost everything is unique and easy to recognize: 2006 (year), 01 (month), 02 (day), 15 (hour in 24-hour format), 04 (minutes), 05 (seconds). Yes, minutes are 04, and yes, that’s the exact reason people mix up 04 and 05.

How to read 2006-01-02 15:04:05

When you write a layout, you don’t memorize “all the formatting codes.” You learn to recognize a few of the most useful pieces of the reference date and combine them. If you think of a layout as a stencil, then 2006 is the year stencil, 01 is the month stencil, 02 is the day stencil, and so on.

Below is a small table of the most useful “pieces.” You don’t need to cram it, but it’s helpful to keep it nearby for the first couple of days until your eyes get used to it.

Piece in layout What it means Example output
2006
year (4 digits)
2026
06
year (2 digits)
26
01
month (2 digits)
01…12
1
month (no leading zero)
1…12
02
day of month (2 digits)
01…31
2
day of month (no leading zero)
1…31
15
hour in 24-hour format
00…23
03
hour in 12-hour format
01…12
04
minutes
00…59
05
seconds
00…59
Z07:00
time zone as an offset (RFC3339 style)
Z
or
+03:00

Notice something important: if you want the format 2026-01-16, then the layout will be 2006-01-02. If you want 16.01.2026, then the layout will be 02.01.2006. This is not “day-month-year with letters,” it’s “day-month-year as the reference.”

3. Formatting and parsing

Formatting: t.Format(layout)

Formatting is when you already have a time.Time and you want to get a string. For example, to nicely display a deadline date in the console, write it to a log, or show it in a report. In Go, this is the Format method on time: t.Format(layout).

Let’s start simple: make a “human” format without a time zone.

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2026, 1, 16, 9, 30, 0, 0, time.UTC)
	fmt.Println(t.Format("2006-01-02 15:04:05")) // 2026-01-16 09:30:00
}

Here 2006-01-02 15:04:05 is simply our stencil. If you need it “European style”: day.month.year—no problem:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2026, 1, 16, 9, 30, 0, 0, time.UTC)
	fmt.Println(t.Format("02.01.2006")) // 16.01.2026
}

time.RFC3339: the standard machine-friendly format

If you output time not only “for human eyes,” but also so it can be reliably read back by a program (or another service), it’s very beneficial to use the standard RFC3339 format. It looks roughly like this: 2026-01-16T09:30:00Z or 2026-01-16T12:30:00+03:00. In Go, it’s already available as a constant: time.RFC3339.

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now().UTC()
	fmt.Println(now.Format(time.RFC3339)) // for example: 2026-01-16T18:42:10Z
}

And this is the case where “standards are boring” is a compliment. RFC3339 is recognized almost everywhere: APIs, logs, databases, queues.

Parsing: time.Parse(layout, s)

Parsing is the reverse operation. We have a string (usually from user input, config, or a file), and we need a time.Time. In Go, this is the time.Parse(layout, s) function, which returns (time.Time, error).

Here it’s important not to fall into “beginner optimism”: if err != nil, the time will be invalid, and the resulting value will often turn out to be the zero time. So a parsing error is not “oh, I’ll deal with it later,” but a mandatory check.

package main

import (
	"fmt"
	"time"
)

func main() {
	s := "2026-01-16 09:30:00"
	t, err := time.Parse("2006-01-02 15:04:05", s)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}
	fmt.Println(t.Format(time.RFC3339)) // 2026-01-16T09:30:00Z
}

Notice: we parsed a string without an explicit time zone, but then printed it as RFC3339 and suddenly got Z (UTC). That’s not magic, but a convention of time.Parse: if there’s no zone in the string, Go parses it as UTC (for this lecture, it’s enough to remember the practical consequence).

If the string has no zone: time.ParseInLocation

When you parse a date “without a zone,” the question always comes up: “in which zone should this be interpreted?” If a person typed 2026-01-16 09:30, they usually mean local time (for example, their city), not UTC. But time.Parse does not read the user’s mind by default.

For this case there is time.ParseInLocation(layout, s, loc). It says: “if there’s no time zone in the string, assume it’s this zone.”

To avoid diving into complex time zone databases, we’ll use a fixed zone via time.FixedZone.

package main

import (
	"fmt"
	"time"
)

func main() {
	loc := time.FixedZone("UTC+3", 3*60*60)

	t, err := time.ParseInLocation("2006-01-02 15:04", "2026-01-16 09:30", loc)
	if err != nil {
		fmt.Println("parse error:", err)
		return
	}

	fmt.Println(t.Format(time.RFC3339)) // 2026-01-16T09:30:00+03:00
}

The idea is simple: ParseInLocation is useful specifically for “human input,” when there is no Z or +03:00 in the string, but you still want to interpret the moment correctly.

4. Example in a training app and debugging

Task deadline: string ↔ time.Time

So that all of this doesn’t remain “magic for magic’s sake,” let’s integrate formatting and parsing into our training application—a small task tracker. For now, without files and without CLI commands: we simply model a task with a struct and print it neatly.

Let’s imagine we already have a model:

package main

import "time"

type Task struct {
	Title string
	DueAt time.Time
}

Now let’s make two small functions: one parses a deadline from a string, the other formats it for output. Important: the parsing format and the output format don’t have to be the same, but in a training app it’s better to keep them stable to reduce confusion.

package main

import "time"

const dueLayout = "2006-01-02 15:04"

func parseDue(s string) (time.Time, error) {
	return time.Parse(dueLayout, s)
}

And now output:

package main

import "fmt"

const dueLayout = "2006-01-02 15:04"

func printTask(t Task) {
	fmt.Printf("TODO: %s (due: %s)\n", t.Title, t.DueAt.Format(dueLayout))
}

Let’s assemble a mini-scenario in main: the user enters a string, we parse it, create a task, and print it.

package main

import (
	"fmt"
	"time"
)

const dueLayout = "2006-01-02 15:04"

type Task struct {
	Title string
	DueAt time.Time
}

func main() {
	due, err := time.Parse(dueLayout, "2026-01-16 09:30")
	if err != nil {
		fmt.Println("bad due:", err)
		return
	}

	task := Task{Title: "buy coffee and don't forget the deadline", DueAt: due}
	fmt.Println(task.DueAt.Format(time.RFC3339)) // 2026-01-16T09:30:00Z
}

Yes, the output came out in UTC (Z), because we used Parse. If what you actually need is “the user’s local time,” you would use ParseInLocation (and this is a great reason to agree in advance in your app: “what does a deadline without a zone mean”).

How to understand what exactly got parsed

When something goes wrong, time can be very convincing: you look at the output, and it seems “close to the truth,” but tasks suddenly become overdue by three hours. So it’s useful to keep a couple of diagnostic techniques that don’t require shamanism or lots of experience.

The simplest technique is to print time in RFC3339 and also check the zone and whether it’s “zero.” In Go this is easily done via fmt.Printf and the Location()/IsZero() methods.

package main

import (
	"fmt"
	"time"
)

func main() {
	t, _ := time.Parse("2006-01-02 15:04", "2026-01-16 09:30")
	fmt.Printf("t=%v\n", t.Format(time.RFC3339)) // 2026-01-16T09:30:00Z
	fmt.Printf("loc=%v\n", t.Location())         // UTC
	fmt.Printf("isZero=%v\n", t.IsZero())        // false
}

If you see isZero=true, it means somewhere you got the “zero time” (for example, you ignored the parsing error or didn’t assign the value). And if the zone is UTC but you expected local time, it means you parsed with the wrong tool.

5. Common mistakes with layout formatting and parsing

Mistake #1: writing a layout like YYYY-MM-DD and expecting Go to “figure it out.”
This is the most common trap. In Go, YYYY does not mean a year, and MM does not mean a month. A layout is not “a set of letter tokens,” but a sample string with the reference date. If you wrote YYYY, Go will treat it as plain text and output it as-is: YYYY. You need to write 2006-01-02.

Mistake #2: mixing up 15, 04, and 05.
Hours in 24-hour format are 15, minutes are 04, seconds are 05. Yes, it’s weird. Yes, almost everyone writes “15:05” at least once and gets seconds instead of minutes. The best cure is to keep 15:04:05 in your head as a single mantra and not try to invent your own mapping.

Mistake #3: ignoring the error from time.Parse.
Parsing returns (time.Time, error) for a reason. If err != nil, you did not get a correct time. Ignoring the error turns a bug into a “silent” one: later you compare dates, build deadlines, and then wonder why everything works “almost always.”

Mistake #4: not thinking about the time zone when the input has no zone.
The string 2026-01-16 09:30 by itself does not say whether it’s UTC or local time. time.Parse interprets it one way, time.ParseInLocation another. If deadlines matter in your program, make sure to lock in a rule: either you accept/store everything in UTC (and honestly show that), or you interpret input as the user’s local time (and then use ParseInLocation).

Mistake #5: formatting “however it comes out” and getting unstable output.
fmt.Println(t) does output something, but it’s not always user-friendly and not always stable for parsing back. It’s better to choose one clear format for the UI (2006-01-02 15:04) and one standard format for the “machine world” (time.RFC3339)—and use them deliberately rather than accidentally.

1
Task
Go SELF, level 34, lesson 2
Locked
Report date
Report date
1
Task
Go SELF, level 34, lesson 2
Locked
Log normalization
Log normalization
1
Task
Go SELF, level 34, lesson 2
Locked
Shift deadline
Shift deadline
1
Task
Go SELF, level 34, lesson 2
Locked
Merging timelines
Merging timelines
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION