strftime

May 21, 2026 · View on GitHub

Fast strftime for Go

Go Reference

SYNOPSIS

f, err := strftime.New(`.... pattern ...`)
if err := f.Format(buf, time.Now()); err != nil {
    log.Println(err.Error())
}

DESCRIPTION

The goals for this library are

  • Optimized for the same pattern being called repeatedly
  • Be flexible about destination to write the results out
  • Be as complete as possible in terms of conversion specifications

API

Format(string, time.Time) (string, error)

Takes the pattern and the time, and formats it. This function is a utility function that recompiles the pattern every time the function is called. If you know beforehand that you will be formatting the same pattern multiple times, consider using New to create a Strftime object and reuse it.

New(string) (*Strftime, error)

Takes the pattern and creates a new Strftime object.

obj.Pattern() string

Returns the pattern string used to create this Strftime object

obj.Format(io.Writer, time.Time) error

Formats the time according to the pre-compiled pattern, and writes the result to the specified io.Writer

obj.FormatString(time.Time) string

Formats the time according to the pre-compiled pattern, and returns the result string.

SUPPORTED CONVERSION SPECIFICATIONS

patterndescription
%Anational representation of the full weekday name
%anational representation of the abbreviated weekday
%Bnational representation of the full month name
%bnational representation of the abbreviated month name
%C(year / 100) as decimal number; single digits are preceded by a zero
%cnational representation of time and date
%Dequivalent to %m/%d/%y
%dday of the month as a decimal number (01-31)
%ethe day of the month as a decimal number (1-31); single digits are preceded by a blank
%Fequivalent to %Y-%m-%d
%Gthe ISO week year with century as a decimal number with 4 digits
%gthe ISO week year without century as a decimal number (00-99) with 2 digits
%Hthe hour (24-hour clock) as a decimal number (00-23)
%hsame as %b
%Ithe hour (12-hour clock) as a decimal number (01-12)
%jthe day of the year as a decimal number (001-366)
%kthe hour (24-hour clock) as a decimal number (0-23); single digits are preceded by a blank
%lthe hour (12-hour clock) as a decimal number (1-12); single digits are preceded by a blank
%Mthe minute as a decimal number (00-59)
%mthe month as a decimal number (01-12)
%na newline
%pnational representation of either "ante meridiem" (a.m.) or "post meridiem" (p.m.) as appropriate.
%Requivalent to %H:%M
%requivalent to %I:%M:%S %p
%Sthe second as a decimal number (00-60)
%Tequivalent to %H:%M:%S
%ta tab
%Uthe week number of the year (Sunday as the first day of the week) as a decimal number (00-53)
%uthe weekday (Monday as the first day of the week) as a decimal number (1-7)
%Vthe week number of the year (Monday as the first day of the week) as a decimal number (01-53)
%vequivalent to %e-%b-%Y
%Wthe week number of the year (Monday as the first day of the week) as a decimal number (00-53)
%wthe weekday (Sunday as the first day of the week) as a decimal number (0-6)
%Xnational representation of the time
%xnational representation of the date
%Ythe year with century as a decimal number
%ythe year without century as a decimal number (00-99)
%Zthe time zone name
%zthe time zone offset from UTC
%%a '%'

NO-PADDING FLAG

A - (glibc) or # (Windows) flag may be placed between the % and the conversion specifier to suppress the leading zero/blank padding on numeric fields. For example, given 2006-01-02 03:04:05:

patternresult
%m01
%-m1
%d02
%-d2
%H:%M03:04
%-H:%-M3:4

The flag has no effect on non-numeric fields (e.g. %-A is identical to %A).

LOCALIZATION

By default the name-producing specifiers (%A, %a, %B, %b, %h, %p) emit English. To localize them, build a Locale with NewLocale and pass it via WithLocale. The library ships no locale data of its own — you supply the names for your language:

french := strftime.NewLocale(
  strftime.WithMonths(strftime.MonthNames{
    "janvier", "février", "mars", "avril", "mai", "juin",
    "juillet", "août", "septembre", "octobre", "novembre", "décembre",
  }),
  strftime.WithWeekdays(strftime.WeekdayNames{
    "dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi",
  }),
  // WithShortMonths, WithShortWeekdays, WithMeridiem ... optional
)

s, _ := strftime.New(`%A %d %B %Y`, strftime.WithLocale(french))
// -> "lundi 02 janvier 2006"

MonthNames is indexed by month minus one (January is index 0); WeekdayNames is indexed by time.Weekday (Sunday is index 0). Any name left empty falls back to the English default, so a partial Locale never yields blank output. Numeric specifiers (%d, %m, %Y, ...) are locale-invariant and unaffected.

Locale is an interface, so you can also implement it yourself to back the names with a map, computed values, or an external dataset. DefaultLocale() returns the English implementation.

Inflected languages

In some languages (Russian, Czech, Polish, Greek, ...) a month name changes form depending on whether it stands alone or appears next to a day number — e.g. Russian "январь" (stand-alone) vs "2 января" (in a date). Because a single Locale carries one form per name, format each context with its own compiled Strftime:

inDate, _   := strftime.New(`%d %B %Y`, strftime.WithLocale(ruInDate))   // января
header, _   := strftime.New(`%B %Y`,    strftime.WithLocale(ruStandalone)) // январь

EXTENSIONS / CUSTOM SPECIFICATIONS

This library in general tries to be POSIX compliant, but sometimes you just need that extra specification or two that is relatively widely used but is not included in the POSIX specification.

For example, POSIX does not specify how to print out milliseconds, but popular implementations allow %f or %L to achieve this.

For those instances, strftime.Strftime can be configured to use a custom set of specifications:

ss := strftime.NewSpecificationSet()
ss.Set('L', ...) // provide implementation for `%L`

// pass this new specification set to the strftime instance
p, err := strftime.New(`%L`, strftime.WithSpecificationSet(ss))
p.Format(..., time.Now())

The implementation must implement the Appender interface, which is

type Appender interface {
  Append([]byte, time.Time) []byte
}

For commonly used extensions such as the millisecond example and Unix timestamp, we provide a default implementation so the user can do one of the following:

// (1) Pass a specification byte and the Appender
//     This allows you to pass arbitrary Appenders
p, err := strftime.New(
  `%L`,
  strftime.WithSpecification('L', strftime.Milliseconds),
)

// (2) Pass an option that knows to use strftime.Milliseconds
p, err := strftime.New(
  `%L`,
  strftime.WithMilliseconds('L'),
)

Similarly for Unix Timestamp:

// (1) Pass a specification byte and the Appender
//     This allows you to pass arbitrary Appenders
p, err := strftime.New(
  `%s`,
  strftime.WithSpecification('s', strftime.UnixSeconds),
)

// (2) Pass an option that knows to use strftime.UnixSeconds
p, err := strftime.New(
  `%s`,
  strftime.WithUnixSeconds('s'),
)

If a common specification is missing, please feel free to submit a PR (but please be sure to be able to defend how "common" it is)

List of available extensions

PERFORMANCE / OTHER LIBRARIES

The benchmarks live under bench/ and compare this library against several others.

// AMD Ryzen 9 7900X3D, Linux/amd64
// go version go1.26.1 linux/amd64
% go test -benchmem -bench .
goos: linux
goarch: amd64
pkg: github.com/lestrrat-go/strftime/bench
cpu: AMD Ryzen 9 7900X3D 12-Core Processor
BenchmarkTebeka-24                        	  728451	      1458 ns/op	     260 B/op	      20 allocs/op
BenchmarkJehiah-24                        	 1898193	       622.1 ns/op	     256 B/op	      17 allocs/op
BenchmarkFastly-24                        	 1356129	       881.0 ns/op	     168 B/op	       6 allocs/op
BenchmarkNcruces-24                       	 5115555	       230.7 ns/op	      64 B/op	       1 allocs/op
BenchmarkNcrucesAppend-24                 	 6263023	       199.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkLestrrat-24                      	 5860896	       206.4 ns/op	     128 B/op	       2 allocs/op
BenchmarkLestrratCachedString-24          	 6105082	       189.2 ns/op	     128 B/op	       2 allocs/op
BenchmarkLestrratCachedWriter-24          	 6648992	       168.7 ns/op	      64 B/op	       1 allocs/op
BenchmarkLestrratCachedFormatBuffer-24    	 8669540	       136.7 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/lestrrat-go/strftime/bench	13.281s

This library is the fastest of the bunch across every access pattern. The annotated list below ranks the relevant variants from fastest to slowest:

Import Pathns/opallocsNote
github.com/lestrrat-go/strftime136.70FormatBuffer() into a reused slice (cached)
github.com/lestrrat-go/strftime168.71Format() to an io.Writer (cached)
github.com/lestrrat-go/strftime189.22FormatString() (cached)
github.com/ncruces/go-strftime199.20AppendFormat()
github.com/lestrrat-go/strftime206.42package-level Format() (compiled patterns are cached)
github.com/ncruces/go-strftime230.71Format()
github.com/jehiah/go-strftime622.117
github.com/fastly/go-utils/strftime881.06
github.com/tebeka/strftime145820

The fastest path is reusing a Strftime object and appending into a slice you own (FormatBuffer), which allocates nothing. The package-level Format() caches compiled patterns internally (bounded), so even repeated one-off calls with the same pattern stay fast.

However, depending on your pattern, this speed may vary. If you find a particular pattern that seems sluggish, please send in patches or tests.

Please also note that this benchmark only uses the subset of conversion specifications that are supported by ALL of the libraries compared.

Somethings to consider when making performance comparisons in the future:

  • Can it write to io.Writer?
  • Which %specification does it handle?