How to parse Unix timestamps in Go

Avatar of the author Willem Schots
2 Jan, 2024
~4 min.
RSS

Unix timestamps are a common way to format timestamps. They’re rather convenient because they’re formatted as number (usually integers).

However, in Go we often we often want “time values” to have the time.Time type because that’s what the rest of the time package works with.

To convert logs or other textual data to time.Time values we need to take a two-step approach:

  1. Parse the text to a number.
  2. Pass that number to the right time.Unix* function.

However, there are some nuances. Not every system, language or library formats Unix timestamps the exact same way. There is often a difference in precision and sometimes fractional notation is used.

To succesfully parse Unix timestamps you will need to know what format your data uses.

What is Unix Time?

Unix timestamps are an expression of "Unix Time". A date and time representation that has its roots in the development of the Unix operating system.

Unix Time measures the number of seconds that have elapsed since "the (Unix) Epoch": 00:00:00 UTC on 1 January 1970. Which is why it's sometimes called "Epoch time".

Every day has exactly 86400 seconds, Unix Time does not adjust for leap seconds.

Precision

Unix timestamps are sometimes formatted in a higher resolution than seconds, modern computers are pretty fast after all:

  • Milliseconds (1000th of a second).
  • Microseconds (1000000th of a second).
  • Nanoseconds (1000000000th of a second).

The Go time package provides several functions to create time.Time values from inputs of different precisions:

  • time.Unix takes seconds and nanosecond inputs. It sums the two inputs.
  • time.UnixMicro takes microseconds as input.
  • time.UnixMilli takes milliseconds as input.

The example at the bottom of the page shows how they work in detail.

Fractional seconds (timestamp with dot)

Sometimes Unix timestamps contain a fractional component and look like this:

1704196185.8095

This usually means that the decimal part are whole seconds, and the fractional part is a fraction of a second.

To parse timestamps like this into Go time.Time values I take the following approach:

  1. Call strings.Split(txt, ".") to split on the decimal seperator.
  2. Parse the decimal part and fractional part seperately.
  3. Multiply the fractional part with 100000000 to go from deciseconds to nanoseconds.
  4. Pass both values to time.Unix to construct a time.Time value.

A concrete example is shown below in the ParseUnixFrac function.

Local Location

The times returned by the time.Unix* functions all are in the local location: time.Local.

Depending on your system configuration, you might want to convert them to another location (like UTC). This can be done using the t.UTC or t.In methods.

If you’re unsure what any of this means, check out this article on Time and Location.

main.go
package main

import (
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"
)

func mustParseInt64(s string) int64 {
	d, err := strconv.ParseInt(s, 10, 0)
	if err != nil {
		log.Fatalf("failed to parse int: %v", err)
	}
	return d
}

func main() {
	// Step 1. first we need to parse strings to integers.
	var (
		sec   = mustParseInt64("1704209323")
		milli = mustParseInt64("1704209323000")
		micro = mustParseInt64("1704209323000000")
		nano  = mustParseInt64("1704209323000000000")
	)

	// Step 2. Pass the integer values to the right functions.
	tsec := time.Unix(sec, 0)
	tmilli := time.UnixMilli(milli)
	tmicro := time.UnixMicro(micro)
	tnano := time.Unix(0, nano)

	// Should all print the same.
	fmt.Println(tsec)
	fmt.Println(tmilli)
	fmt.Println(tmicro)
	fmt.Println(tnano)

	// Half a second after the earlier timestamps.
	tfrac, err := ParseUnixFrac("1704209323.5")
	if err != nil {
		log.Fatalf("failed to parse fractional unix timestamp: %v", err)
	}

	fmt.Println(tfrac)
}

// ParseUnixFrac parses an Unix timestamp that uses fractional seconds.
// It assumes a decimal and fractional part are both present.
func ParseUnixFrac(s string) (time.Time, error) {
	// split on the decimal seperator '.'
	parts := strings.Split(s, ".")
	if len(parts) != 2 {
		return time.Time{}, fmt.Errorf("expected decimal and fractional parts, got %d part", len(parts))
	}

	// parse the decimal and fractional parts.
	decimal, err := strconv.ParseInt(parts[0], 10, 0)
	if err != nil {
		return time.Time{}, fmt.Errorf("failed to parse decimal part: %v", err)
	}

	frac, err := strconv.ParseInt(parts[1], 10, 0)
	if err != nil {
		return time.Time{}, fmt.Errorf("failed to parse decimal part: %v", err)
	}

	// multiply from deciseconds to nanoseconds for the fractional part.
	return time.Unix(decimal, frac*100_000_000), nil
}

🎓

Subscribe to my Newsletter and Keep Learning.

Gain access to more content and get notified of the latest articles:

I send emails every 1-2 weeks and will keep your data safe. You can unsubscribe at any time.

Hello! I'm the Willem behind willem.dev

I created this website to help new Go developers, I hope it brings you some value! :)

You can follow me on Mastodon, Twitter/X or LinkedIn.

Thanks for reading!