Where does time.LoadLocation get zone data, and what does importing time/tzdata change?
answer
- four places, checked in order
- an environment variable can override the search
- the last resort is compiled into the binary
- a blank import for its side effect only
- roughly 450 KB, and main decides
basics
~20 stime.LoadLocation searches the ZONEINFO directory or zip, then the system zoneinfo directory, then $GOROOT/lib/time/zoneinfo.zip, then the copy embedded by time/tzdata if that package is imported. Blank-importing time/tzdata makes a binary work where no zone files exist.
solid answer
~40 s`time.LoadLocation(name)` resolves `""` and `"UTC"` to UTC and `"Local"` to `time.Local`; any other name is looked up in the IANA database. It searches, in order: the directory or uncompressed zip named by the `ZONEINFO` environment variable, the system zoneinfo directory on Unix, `$GOROOT/lib/time/zoneinfo.zip`, and finally the copy embedded by `time/tzdata` if that package has been imported. On a machine with none of those it returns an error and a nil `*time.Location`. A blank import — `import _ "time/tzdata"` — embeds the whole IANA database in the binary, roughly 450 KB, as a fallback used only when the system files are missing. It belongs in the program's `main` package, not in a library, because embedding is the binary owner's decision.
code
go · 13 linesimport (
"time"
_ "time/tzdata" // fallback copy of the IANA database
)
func mustLoadZone(name string) *time.Location {
loc, err := time.LoadLocation(name)
if err != nil {
panic("zone " + name + ": " + err.Error()) // never silently use UTC
}
return loc
}go deeper
Know that LoadLocation reads an IANA database that lives outside your program, that it therefore returns an error, and that a blank import of time/tzdata can put a copy inside the binary.
Be able to recite the search order — ZONEINFO, the system zoneinfo directory, GOROOT's zoneinfo.zip, then the embedded copy — and explain why a lookup succeeds on a laptop and fails in a minimal image.
Demonstrate the operational habit: validate every configurable zone at startup, refuse to fall back to UTC silently, and reproduce the missing-database case in the deployment image and in CI rather than trusting the developer machine.
Set the standard for the fleet: whether binaries carry their own zone data or the base image supplies it, how that choice is verified in the build, and who is accountable when the two drift apart.
## The lookup chain `time.LoadLocation(name string) (*time.Location, error)` handles three special names first: `""` and `"UTC"` return `time.UTC`, and `"Local"` returns `time.Local`. Anything else, such as `"America/New_York"` or `"Europe/Berlin"`, is treated as a location name in the IANA time zone database (often called tzdata or zoneinfo), and Go goes looking for that database in a fixed order: 1. **The `ZONEINFO` environment variable** — a directory, or an *uncompressed* zip file, holding zone files. This is the override hook: point it at a directory you control and every lookup uses that copy. 2. **The system installation** — on Unix-like systems the standard location, typically `/usr/share/zoneinfo`, populated by an OS package. 3. **`$GOROOT/lib/time/zoneinfo.zip`** — the copy that ships with a Go installation. Useful on a developer machine with a Go toolchain installed; absent from a deployment image that only carries the compiled binary. 4. **The embedded copy from `time/tzdata`** — present only if that package has been imported somewhere in the program. The first source that yields the requested zone wins. If none does, `LoadLocation` returns a nil `*Location` and a non-nil error. ## Why this bites in deployment and not in development A developer machine has all of sources 2 and 3, so `LoadLocation` always succeeds and the error branch is never exercised. A minimal deployment image — a base image built to contain nothing but the binary — has neither the OS zone package nor a Go installation, so every lookup fails. Two symptoms follow, and both look like something else: - Code that handles the error by falling back to `time.UTC` renders every user's wall-clock time in UTC. Nothing crashes, nothing logs, and the times are plausible; they are just wrong by the user's offset. - Code that ignores the error stores a nil `*time.Location`, and the program panics later inside `Time.In`, far from the real cause. The same missing-database condition also makes `time.Local` UTC, since the process cannot resolve a local zone either. ## What the blank import does ```go import _ "time/tzdata" ``` `time/tzdata` contains a compiled-in copy of the IANA database. Importing it for its side effect registers that copy with the `time` package as the last-resort source. It is a **fallback**, not a replacement: system zone files, when present, are still used first. The cost is roughly 450 KB of binary size, and the package's own guidance is that a program's `main` package should import it, never a library — a library that embeds the database imposes that size on every program that depends on it, and cannot know whether the deployment already provides zone files. ## The narrower alternative `time.LoadLocationFromTZData(name string, data []byte) (*time.Location, error)` builds a `*Location` from a byte slice already in IANA format. That lets you embed just the handful of zones you actually need (or a whole database blob you control) with `//go:embed`, pin its content in your own repository, and avoid the general 450 KB. The trade is that you must now source, verify and update that data yourself, and any zone outside your set fails. ## Verifying it, rather than assuming it The failure is a *deployment* property, so reproduce it where deployment lives: - Run the binary in the target image with `ZONEINFO` unset and print `time.Local.String()` plus the result of a `LoadLocation` for a real zone. UTC and an error together is the signature. - Add a startup check that loads every zone the service can be configured with and refuses to start on failure. A service that must render local times has no useful degraded mode, and failing loudly at boot is far cheaper than an hour-shifted schedule discovered by a user. - Add a test that runs with `ZONEINFO` pointed at a nonexistent path, so the error branch is exercised in CI rather than in production. ## Rules that follow - Never write `loc, _ := time.LoadLocation(name)`. The error is not decorative; it is the whole failure mode. - Never fall back to `time.UTC` on error unless UTC is genuinely correct for the caller — silent substitution converts a loud failure into wrong data. - Load each location once at startup and reuse the `*Location`; `LoadLocation` re-reads and re-parses, and `Location` values are safe for concurrent use. - Decide deliberately, and write down, whether the binary carries its own database or trusts the image to provide one.
- Why does the time/tzdata documentation tell libraries not to import it?Because embedding costs every dependent binary roughly 450 KB, and only the binary's owner knows whether the deployment already ships zone files. A library that blank-imports it makes that call for programs it cannot see. The `main` package imports it; libraries just call `LoadLocation` and report errors.
- What is ZONEINFO good for beyond production overrides?It pins which database a process uses. Pointing it at a fixed directory or uncompressed zip makes zone behaviour reproducible across machines and CI, and pointing it at a nonexistent path is the cheapest way to exercise the missing-database error branch in a test.
- When would you use time.LoadLocationFromTZData instead of the tzdata import?When you need only a few zones, or need the exact database bytes under your own version control. `LoadLocationFromTZData` builds a `*time.Location` from IANA-format bytes you supply, typically via `//go:embed`, keeping the binary small and the data pinned — at the cost of owning updates and failing for any zone you did not ship.
saying these in an interview costs you the question
- Says LoadLocation always works because Go ships the database
- Handles a LoadLocation error by returning time.UTC
- Writes loc, _ := time.LoadLocation(name)
- Thinks time/tzdata replaces rather than backs up system zone files
- Blank-imports time/tzdata from a shared library
- Believes the embedded database is fetched or refreshed at run time