Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/timeutil"
Two types that serialise to human-readable strings where the standard library does not: DateTime[T] for timestamps in a layout chosen at compile time, and Duration for "1h30m" instead of a nanosecond integer.
The two mismatches
time.Duration marshals to JSON as a bare nanosecond integer. A thirty-minute timeout leaves your service as 1800000000000, which no human reads correctly and no other language parses back into a duration without knowing the unit. timeutil.Duration marshals as "30m0s", the same string time.Duration.String() produces and time.ParseDuration accepts.
time.Time marshals as RFC3339 and nothing else. Any API that specifies a different layout, and there are many, needs a wrapper type per layout, each with near-identical MarshalJSON and UnmarshalJSON methods.
The layout is a type parameter
DateTime[T DateTimeType] takes the format from its type parameter:
type Event struct {
At timeutil.DateTime[timeutil.TRFC3339] `json:"at"`
}
type JiraIssue struct {
Created timeutil.DateTime[timeutil.TJira] `json:"created"`
}
DateTimeType is a single-method interface, Format() string. Every layout in the standard library has a type: TRFC3339, TRFC3339Nano, TRFC822, TRFC822Z, TRFC850, TRFC1123, TRFC1123Z, TUnixDate, TANSIC, TRubyDate, TKitchen, TStamp, TStampMilli, TStampMicro, TStampNano, TLayout, TDateTime, TDateOnly, TTimeOnly. TJira covers Jira’s timestamp format, used by jirasrv.
Putting the layout in the type rather than in a struct tag or a constructor argument means a mismatch is a compile error rather than a runtime parse failure. It also means the format is visible in the field declaration, so a reader of the struct knows the wire format without finding the marshaller.
Defining your own layout takes a type with one method:
type TCompact struct{}
func (TCompact) Format() string { return "20060102150405" }
type Record struct {
Stamp timeutil.DateTime[TCompact] `json:"stamp"`
}
Text marshalling, not just JSON
Both types implement encoding.TextMarshaler and encoding.TextUnmarshaler alongside the JSON interfaces. Three consequences:
- They work as JSON map keys, where Go requires a text marshaller rather than a JSON one.
- They work with YAML and TOML decoders, so one type serves a config file and an API payload.
- They work with
flag.TextVar, so a duration or a timestamp can be a command-line flag with parsing and validation already attached.
The map-key case is the one people hit unexpectedly: implementing only MarshalJSON produces a compile-time-valid type that fails at runtime when used as a key.
Getting the underlying values back
DateTime.Time() returns the wrapped time.Time, and String() renders it in the parameterised layout. Duration is a defined type over time.Duration, so a conversion gets you back to the standard type for arithmetic.
The wrappers only change serialisation. Comparison, arithmetic and time zone handling go through the standard library unchanged.
Example
type Config struct {
Timeout timeutil.Duration `json:"timeout"`
}
var c Config
err := json.Unmarshal([]byte(`{"timeout":"1h30m"}`), &c)
fmt.Println(c.Timeout, err)
Full source is in example_duration_test.go, with the datetime equivalents in example_datetime_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library.
Links
- Reference documentation
- Source code
- The Jira layout in use: jirasrv
- Configuration loading: config
- All nurago packages