
This post was included in Golang Weekly issue 499. This is cross-posted from the Hardfin engineering blog.
The Go standard library uses a single overloaded type as a stand-in for both full datetimes1 and dates. This mostly "just works", but slowly starts to degrade correctness in codebases where both datetime and date values need to interact.
We store events using a mix of datetime and date columns in our database
(depending on the type of event). Having a mix of datetimes and date values
became a persistent source of subtle confusion and extended discussion2 within our engineering team. As a result, we
created a simple Date type that organically grew until it could replace all of
our existing usage of time.Time.
The go-date package is here to fill the gap left by the Go standard
library!
Having a mix of datetimes and date values became a persistent source of subtle confusion and extended discussion within our engineering team
Opportunity for correctness to degrade
To understand an example where correctness can start to degrade, consider
a datetime value (now) and a date value (deadline):
now := time.Now()
// Common convention for date values is to use a `time.Time` with
// only the year, month, and day set. For example, this convention
// is followed by the standard library when a timestamp of the form
// YYYY-MM-DD is parsed via `time.Parse(time.DateOnly, value)`.
deadline := time.Date(2024, time.March, 1, 0, 0, 0, 0, time.UTC)
With two time.Time values, it's very reasonable for a developer to
compute the check "current date is on or before the deadline" via:
withinDeadline := !now.After(deadline)
However, this is problematic for at least two reasons. First and foremost, the
timestamp of deadline is 00:00, so it leaves out virtually 100% of the
deadline day. Secondly, the concept of "today" depends3
on the timezone and for many applications, the application user likely has a
specific timezone that is natural for that check.
In codebases maintained over time by teams of different people, it's very common for a developer to come in and edit existing code that they didn't write. In cases like this, it's easy to have less context than the original author.
Without sufficient context, mixing up the precision of now (datetime) and the
precision of deadline (date) is an easy mistake to make. On the other hand, if
deadline was not the same type as now, developers are forced to consider the
difference between the two types and account for this difference. If a Date
was used to represent the deadline instead:
deadlineDate := date.NewDate(2024, time.March, 1)
then a correct implementation can both incorporate the timezone and ensure that same percentage of the hours in the day are not erroneously ignored:
nowDate := date.InTimezone(now, tz)
withinDeadline := !nowDate.After(deadlineDate)

Integrating with sqlc
We use the sqlc library at Hardfin for all Go code that interacts with
the database. (See the wonderful post by PostgreSQL blogging legend Brandur
Leach for some of the benefits of sqlc.)
Out of the box, sqlc uses a Go time.Time both for columns of type
TIMESTAMPTZ and DATE. When reading DATE values (which come over the
wire in the form YYYY-MM-DD), the Go standard library produces values of the
form:
time.Date(YYYY, MM, DD, 0, 0, 0, 0, time.UTC)
Instead, we can instruct sqlc to globally use date.Date and
date.NullDate when parsing DATE columns:
---
version: "2"
overrides:
go:
overrides:
- go_type:
import: github.com/hardfinhq/go-date
package: date
type: NullDate
db_type: date
nullable: true
- go_type:
import: github.com/hardfinhq/go-date
package: date
type: Date
db_type: date
nullable: false
Why do we care?
At Hardfin, we help equipment manufacturers automate their financial operations by connecting real world events to hardware subscriptions. This inherently means we collect and store a lot of dates.
For many event types, we are able to collect both the date and the timestamp. For example, if a user action occurs in our application or if an update comes in from a third party system like Stripe. However for some events, the timestamp is not required, not important, and oftentimes not known. For example, an operator may know that "a robot was shipped on December 28" but may not know the exact time of day that robot was shipped. The date December 28 has enough information without the timestamp to trigger automation for the manufacturer's billing and revenue.

Go forth and differentiate
This package is intended to be simple! (Simple to understand and simple to implement.) The package is only intended to cover "modern" dates (i.e. dates between the years 1900 and 2100) and so it can avoid resorting to the proleptic Gregorian calendar.
As a result, the core Date type directly exposes the year, month, and day as
struct fields.
We hope this package will come in handy for teams that need to draw a clear distinction between datetime and date values!

- Here by datetime, we mean an object that contains
both date (year, month, day), time information (hour, minute, second,
millisecond / microsecond / nanosecond), and often a relevant timezone as well.
For example this is provided by the
datetime.datetimetype in Python andDatein JavaScript. ↩ - Those
discussions usually included a phrase like "I wish the Go standard library had a
separate
Datetype"! ↩ - Why does the "today" depend on the timezone? If a friend from Los Angeles calls at 11pm, but you're in New York, today is tomorrow! (Or yesterday, depending on who says it.) ↩