If you've written any Go for a while, you know the time package is both elegant and... quirky. The infamous "reference time" (Mon Jan 2 15:04:05 MST 2006) still catches people off guard. But when it comes to Unix timestamps, Go actually shines. The API is clean, the footguns are fewer, and once you learn a few patterns, you'll wonder why other languages make it so hard.

Let's walk through what it's like to work with Unix timestamps in Go — from real APIs tossing 13-digit numbers at you, to formatting timestamps for users around the world.

The Basics: Unix Timestamp to time.Time

A Unix timestamp is just an integer — seconds (or milliseconds) since January 1, 1970 UTC. Converting one to a Go time.Time is straightforward:

import "time"

func main() {
    // Timestamp in seconds (10 digits)
    var ts int64 = 1719820800
    t := time.Unix(ts, 0)
    fmt.Println(t) // 2024-07-01 08:00:00 +0800 CST
}

Wait — why does it show 08:00:00? Because time.Unix() returns a time.Time in the local time zone of the machine. That's the default behavior. If you want UTC, just call .UTC():

t := time.Unix(ts, 0).UTC()
fmt.Println(t) // 2024-07-01 00:00:00 UTC

Seconds vs Milliseconds: The Eternal Confusion

A timestamp from a JavaScript frontend or a Java backend often comes in milliseconds (13 digits). Feed that directly into time.Unix() and you'll get a date tens of thousands of years in the future — way past your retirement.

// Wrong!
ts := 1719820800000 // milliseconds
t := time.Unix(ts, 0) // year ~56468 — not what you want

The fix is simple: time.UnixMilli() was added in Go 1.17.

t := time.UnixMilli(ts).UTC()
fmt.Println(t) // 2024-07-01 00:00:00 UTC

There's also time.UnixMicro() for microsecond precision. If you're stuck on an older Go version, just divide:

t := time.Unix(ts/1000, 0)

Formatting a Timestamp for Humans

Once you have a time.Time, formatting it is where Go's reference time layout catches everyone. Go doesn't use YYYY-MM-DD like every other language. You use 2006-01-02.

t := time.Unix(1719820800, 0).UTC()

// 2006 = year, 01 = month, 02 = day
fmt.Println(t.Format("2006-01-02"))        // 2024-07-01

// 15 = 24-hour hour, 04 = minute, 05 = second
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2024-07-01 00:00:00

// RFC3339 is a common standard
fmt.Println(t.Format(time.RFC3339))          // 2024-07-01T00:00:00Z

You don't need to memorize the reference time. Just remember this sequence:

Mon Jan 2 15:04:05 MST 2006

Or more practically: 2006-01-02 15:04:05. Once you've written it three or four times, the muscle memory kicks in.

Going the Other Way: time.Time to Unix Timestamp

Need to send a timestamp back to the frontend or store it in a database? time.Time has you covered:

t := time.Date(2024, 7, 1, 0, 0, 0, 0, time.UTC)

fmt.Println(t.Unix())       // 1719820800 (seconds)
fmt.Println(t.UnixMilli())  // 1719820800000 (milliseconds)
fmt.Println(t.UnixMicro())  // 1719820800000000 (microseconds)
fmt.Println(t.UnixNano())   // 1719820800000000000 (nanoseconds)

These methods are timezone-aware, meaning they return the absolute epoch time regardless of the time.Time's zone. No surprises there.

Parsing Strings Back to time.Time

When you receive a date string from an API or a CSV file, use time.Parse():

// Parse an RFC3339 string
t, err := time.Parse(time.RFC3339, "2024-07-01T00:00:00Z")
if err != nil {
    panic(err)
}
fmt.Println(t.Unix()) // 1719820800

Custom formats work the same way — but with Go's layout:

// Parse "2024-07-01 00:00:00"
t, err := time.Parse("2006-01-02 15:04:05", "2024-07-01 00:00:00")
if err != nil {
    panic(err)
}

Important: time.Parse() returns a time in UTC by default if no timezone is in the string. If you want it parsed as local time, use time.ParseInLocation():

loc, _ := time.LoadLocation("Asia/Shanghai")
t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2024-07-01 08:00:00", loc)
fmt.Println(t.UTC()) // 2024-07-01 00:00:00 UTC

A Real-World Scenario: Parsing an API Response

Imagine you're hitting a REST API that returns timestamps in seconds, and you need to generate a human-readable report in your local timezone:

type ApiResponse struct {
    CreatedAt int64  `json:"created_at"` // Unix seconds
    Message   string `json:"message"`
}

func formatTimestamp(ts int64) string {
    t := time.Unix(ts, 0)
    return t.Format("2006-01-02 15:04:05 (MST)")
}

func main() {
    resp := ApiResponse{CreatedAt: 1719820800, Message: "Hello"}
    fmt.Println(formatTimestamp(resp.CreatedAt))
    // 2024-07-01 08:00:00 (CST) — on my machine
}

If your app runs on servers in different regions, consider always working in UTC internally and converting to local time only at the display layer. This prevents the classic "server in Virginia, users in Tokyo" off-by-day bug.

Common Mistakes and How to Avoid Them

1. Passing Milliseconds to time.Unix()

As covered above — always check if your timestamp is 10 digits (seconds) or 13 digits (milliseconds). A quick sanity check: if the year looks wildly wrong, you probably forgot UnixMilli().

2. Forgetting that time.Unix() Returns Local Time

t := time.Unix(1719820800, 0)
// This is local time, not UTC! Use .UTC() explicitly.

3. Misunderstanding time.Parse() Timezone Behavior

// This returns UTC, not local time!
t, _ := time.Parse("2006-01-02", "2024-07-01")

If you want to parse a date string as local time, use ParseInLocation. If you always want UTC (recommended for storage), time.Parse() actually helps you here.

4. Using time.Now() Without Considering Precision

fmt.Println(time.Now().Unix())      // seconds
fmt.Println(time.Now().UnixMilli()) // milliseconds

If you're generating timestamps for a primary key or ordering, milliseconds might not be enough under high concurrency. Consider monotonic clock readings or UUIDs instead.

Performance: Time Conversions Are Cheap

One thing Go gets right — time.Unix() and time.Time.Unix() are fast. They boil down to integer arithmetic plus a bit of math. You can happily convert millions of timestamps without worrying about GC pressure or hidden allocations.

Wrapping Up

Go's time package, despite its unusual layout format, provides a clean and predictable API for working with Unix timestamps. The key takeaways:

  • Use time.Unix() for seconds, time.UnixMilli() for milliseconds
  • Remember that time.Unix() returns local time — call .UTC() explicitly when needed
  • The reference layout 2006-01-02 15:04:05 is your friend, not your enemy
  • For string parsing, time.Parse() defaults to UTC; use ParseInLocation for local time

Next time you're debugging a timestamp that seems off by hours or years, check the units first. Nine times out of ten, that's the culprit.

For quick timestamp checks without writing any code, the FastUnix Timestamp Converter is always handy.