How to Add or Subtract Time in Go with time.Duration

Use the `time.Duration` type to represent time intervals and add or subtract them from a `time.Time` value using the built-in `Add()` and `Sub()` methods.

The timeline and the ruler

You are writing a background worker that retries a failed database query. The retry logic needs to wait longer each time: 1 second, then 2 seconds, then 4 seconds. You need to calculate the next wake-up time based on the current moment and a growing delay. You also need to measure how long the query actually took to decide if it was slow. Go handles this with time.Duration and time.Time, but the types are stricter than they look.

Think of time.Time as a specific coordinate on a timeline, like a timestamp on a photo. time.Duration is a length, like the distance between two cities. You can add a distance to a coordinate to find a new location. You can subtract two coordinates to find the distance between them. You cannot add two coordinates together; that makes no sense. Go enforces this distinction with types. time.Time and time.Duration are different types. The compiler stops you from adding two timestamps or subtracting a timestamp from a duration. This design prevents subtle bugs where a timestamp is accidentally treated as an interval.

Core arithmetic

Here's the core arithmetic: add a duration to a time to move forward, subtract two times to measure the gap.

package main

import (
	"fmt"
	"time"
)

func main() {
	// Capture the current moment as a Time value
	now := time.Now()

	// Define a duration using the built-in constant for one hour
	oneHour := time.Hour

	// Add returns a new Time value; it does not modify the receiver
	later := now.Add(oneHour)

	// Subtract two times to get the duration between them
	diff := later.Sub(now)

	// Print the results to verify the arithmetic
	fmt.Printf("Now: %s\n", now.Format(time.RFC3339))
	fmt.Printf("Later: %s\n", later.Format(time.RFC3339))
	fmt.Printf("Diff: %v\n", diff)
}

Under the hood, time.Duration is a signed int64 representing nanoseconds. It is a plain number. It does not know about hours, minutes, or seconds; those are just constants defined in the time package for your convenience. time.Hour is just 60 * time.Minute, which expands to 60 * 60 * time.Second, which is 60 * 60 * 1_000_000_000. When you call now.Add(oneHour), Go adds the nanosecond count to the internal representation of the time.

The time.Time type handles the messy calendar math. It knows how many days are in February, whether it is a leap year, and how daylight saving time shifts the clock. You don't need to write that logic. The method returns a new time.Time value. Go types are immutable in this context; Add does not modify now. You must capture the result. If you call now.Add(time.Hour) without assigning it, the new time is discarded and now remains unchanged.

time.Time stores the instant as seconds and nanoseconds since the Unix epoch, plus a pointer to a time.Location. The location determines how the time is formatted and how wall-clock arithmetic works. When you add a duration, the location pointer is copied. The resulting time is in the same zone. If you cross a daylight saving boundary, the wall clock might jump, but the duration added is exact. Add always adds the exact number of nanoseconds. It does not try to preserve the wall-clock time. If you add 24 hours to a time during a DST transition, the result might be 23 hours or 25 hours of wall-clock difference, but the duration is exactly 24 hours. This distinction matters for scheduling.

Times are points. Durations are spans. The compiler keeps them apart.

Parsing durations from strings

Configuration files and command-line flags often express intervals as strings like "5m" or "1h30s". time.ParseDuration converts these strings into time.Duration values. It is the standard way to accept user-defined time intervals.

Here's a helper that parses a duration string and computes a deadline. It wraps the error to preserve context.

// calculateDeadline parses a duration string and returns the absolute time
// when a task should finish relative to the current moment.
func calculateDeadline(durationStr string) (time.Time, error) {
	// Parse the string into a Duration; returns error on bad format
	d, err := time.ParseDuration(durationStr)
	if err != nil {
		// Wrap the error to include the input value for debugging
		return time.Time{}, fmt.Errorf("invalid duration %q: %w", durationStr, err)
	}

	// Add computes the absolute deadline; the location is preserved from Now
	return time.Now().Add(d), nil
}
func main() {
	// Call the helper with a config-like string
	deadline, err := calculateDeadline("15m")
	if err != nil {
		fmt.Println(err)
		return
	}

	// Output the result using the standard RFC3339 format
	fmt.Printf("Deadline: %s\n", deadline.Format(time.RFC3339))
}

ParseDuration accepts h, m, s, ms, us, ns. It does not accept M for months or d for days. 24h works. 1d returns an error. The error message tells you the unit is unrecognized. Always check the error. If the input is malformed, the function returns a zero duration and a non-nil error. Using the zero duration without checking the error leads to silent logic failures.

The community convention is to use time.RFC3339 for formatting timestamps in logs and APIs. It is the standard for machine-readable time. time.ParseDuration follows a similar convention for input formats. Stick to these standards unless you have a specific reason to deviate.

ParseDuration fails on bad input. Handle the error.

Pitfalls and edge cases

The biggest trap is months. time.Duration has no concept of months because months vary in length. There is no time.Month constant that acts as a duration. If you try to add a month using arithmetic, the compiler rejects it. You get an error like cannot use time.Month(1) as time.Duration value in argument. To add months, use AddDate.

// AddDate handles months and years; it adjusts for varying month lengths
nextMonth := now.AddDate(0, 1, 0)

You cannot use the + operator to add a duration to a time. Go requires the method call. now + time.Hour fails with invalid operation: now + time.Hour (mismatched types time.Time and time.Duration). Use now.Add(time.Hour). This design forces you to be explicit about the operation. It also allows the method to return a new value without mutating the receiver.

The zero value of time.Time is January 1, year 1, 00:00:00 UTC. This is not a valid timestamp for most applications. If you declare a variable var t time.Time, it holds this zero value. Always check t.IsZero() before using a time that might be uninitialized. The compiler cannot catch this because the zero value is a valid struct. Runtime checks are required.

Durations drive the concurrency primitives. time.Sleep takes a duration to block the goroutine. time.After returns a channel that fires after a duration. time.NewTimer creates a reusable timer. All of these expect time.Duration. The type system ensures you don't pass a timestamp to a sleep call. If you accidentally pass a time.Time to time.Sleep, the compiler rejects it with a type mismatch error.

Months break duration math. Use AddDate.

Zero time is year 1. Check IsZero.

Decision matrix

Use Add when you need to shift a timestamp forward or backward by a fixed interval like seconds, minutes, or hours.

Use Sub when you need to measure the elapsed time between two timestamps, returning a time.Duration.

Use ParseDuration when you are reading time intervals from configuration files, flags, or user input as strings.

Use AddDate when you need to add months or years, since time.Duration cannot represent variable-length calendar units.

Use Before or After when you only need to compare two times and don't care about the exact difference.

Use time.Until or time.Since when you want a duration relative to time.Now() without writing the subtraction manually.

Use time.Sleep when you need to pause the current goroutine for a fixed duration.

Use time.After when you need a channel that signals after a duration, typically for timeouts in select statements.

Where to go next