bigywater.tech
Go · 86

时区与 time.Location:加载、夏令时和 Format 的陷阱

时间相关的 bug 多半出在时区和夏令时上。这篇用真实运行验证几个典型问题:跨夏令时加一天、不存在和重复的时刻、字符串解析时的时区、LoadLocation 的代价,以及最容易在上线时才发现的:容器里没有时区数据库。

23 / 25 小时
纽约 2026 年 3 月 8 日与 11 月 1 日这两天各有多少小时
17498 ns
time.LoadLocation 一次的耗时(读文件),t.In 只要 0 ns
+381 KiB
加 -tags timetzdata 后二进制多出的大小,换来容器里能用 LoadLocation

加一天不是加 24 小时

实测 · 本机执行输出
  纽约 2026-03-07 12:00 (春季跳时前一天)+ Add(24h) = 2026-03-08 13:00 EDT;AddDate(0,0,1) = 2026-03-08 12:00 EDT
  两者相差 -1h0m0s
  不存在的时刻 time.Date(2026,3,8,2,30,…,纽约)(当天 2:00 直接跳到 3:00):得到 2026-03-08 01:30 EST -0500
  重复出现的时刻 time.Date(2026,11,1,1,30,…,纽约)(当天 1:30 出现两次):得到 2026-11-01 01:30 EDT -0400
  纽约 3 月 8 日这一天(跳时日)有 23 小时
  纽约 11 月 1 日这一天(回拨日)有 25 小时

纽约 3 月 7 日 12:00 加 24 小时到了 3 月 8 日 13:00 EDT,而 AddDate(0,0,1) 是 12:00 EDT,差一个小时。跳时那天只有 23 小时,回拨那天有 25 小时。要“明天同一时刻”用 AddDate。

不存在和重复的时刻

实测 · 本机执行输出
  time.Parse(无时区的字符串):2026-06-01T12:00:00Z;ParseInLocation(…, 上海):2026-06-01T12:00:00+08:00;两者相差 8h0m0s
  time.Parse(…, "… EST"):err=<nil>,区名 "EST",偏移 0 秒(没有系统认识 EST 时,偏移为 0,不是 −5 小时)
  time.ParseInLocation(…, "… EST", 纽约):区名 "EDT",偏移 -14400 秒(字符串里写的 EST 没有生效,按位置 6 月的实际规则得到 EDT)
  layout "2006-1-2" 解析 "2026-06-01":err=<nil>

3 月 8 日 2:30 并不存在,time.Date 得到 01:30 EST;11 月 1 日 1:30 出现两次,得到的是先出现的 EDT。文档本来就说这种情况下选哪个时区“不保证”,不要依赖。

解析要带位置

实测 · 本机执行输出
  time.LoadLocation("Asia/Shanghai") 17498 ns/次(10 次分配);t.In(已加载的位置) 0 ns/次;t.Format(RFC3339) 34 ns/次

time.Parse 对不带时区的字符串按 UTC,ParseInLocation 才按指定位置;带 EST 这类缩写时,Parse 并不会把它换算成 −5 小时(偏移 0 秒)。解析业务时间时优先用 ParseInLocation,最好用带数字偏移的 RFC3339。

LoadLocation 要缓存

实测 · 本机执行输出
  TZ="":time.Local = UTC;现在 05:57 UTC +0000
  TZ="UTC":time.Local = UTC;现在 05:57 UTC +0000
  TZ="Asia/Shanghai":time.Local = Asia/Shanghai;现在 13:57 CST +0800
  TZ="America/New_York":time.Local = America/New_York;现在 01:57 EDT -0400

每次 LoadLocation 约 17498 ns、10 次分配;加载后 t.In 不要钱。启动时加载一次。

TZ 会改变 Local

实测 · 本机执行输出
  二进制大小:不带 tzdata 2.86 MB;加 -tags timetzdata 3.25 MB(多 381 KiB)
  [普通编译] time.LoadLocation("Asia/Shanghai") 失败:unknown time zone Asia/Shanghai
  [-tags timetzdata] time.LoadLocation("Asia/Shanghai") 成功:20:00 CST +0800

环境变量 TZ 为空串或 UTC 时 time.Local 是 UTC,设成地区名则按该地区。同一份程序在不同机器、不同容器里 Local 可能不同。

容器里的 tzdata

实测 · 本机执行输出
  LoadLocation returns a Location with the given name. If the name is "" or "UTC", LoadLocation returns UTC. If the name is "Local", LoadLocation returns Local. Otherwise, a new Location is created where the name is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York". LoadLocation looks for the IANA Time Zone database in the following locations in order: - the directory or uncompressed zip file named by the ZONEINFO environment variable - on a Unix system, the system standard installation location - $GOROOT/lib/time/zoneinfo.zip - the time/tzdata package, if it was imported

没有 /usr/share/zoneinfo 的最小镜像里,LoadLocation("Asia/Shanghai") 报 unknown time zone;编译时加 -tags timetzdata(或导入 time/tzdata)把数据库嵌进二进制,多 381 KiB。文档里的查找顺序也包括 $GOROOT/lib/time/zoneinfo.zip 与 ZONEINFO 环境变量。

分步演示

时区与 time.Location
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。夏令时与解析实验在 macOS 上用系统 /usr/share/zoneinfo;“容器里”的实验是把同一份源码交叉编译成 linux/arm64 静态程序(CGO_ENABLED=0),用 docker import 做成只含这一个文件的镜像,运行后已删除镜像;耗时用 testing.Benchmark;读过的文档:go doc time.LoadLocation
    • 没有验证:Linux 上系统时区库的路径差异、Windows 的行为、更多夏令时边缘(如半小时偏移的时区)、time/tzdata 的数据库版本是否最新、Format 的各个 layout 细节
    京ICP备2026067502号-1京公网安备11010502063487号