时间看起来很简单,直到你尝试编写正确处理时间的软件。"下午3点"的活动在东京、伦敦和纽约意味着完全不同的时刻。夏令时会将时钟向前或向后调——但并非所有地方都实行,而且日期也不相同。一个在本地环境中看起来正确的时间戳,在生产环境中可能出错,因为服务器在不同的时区。
时区是软件开发中最容易让人困惑的话题之一。本文将解释时区的工作原理、复杂性的来源,以及如何避免最常见的错误。
时区为何存在
地球在24小时内旋转360度,这意味着太阳到达最高点的时刻取决于你所在的经度。在19世纪之前,每个城市都按照当地太阳时来设定时钟——正午就是太阳在头顶的时候。这在铁路连接了相距遥远的城市之前一切正常,因为火车时刻表需要一个统一的时钟。
1884年,来自25个国家的代表在华盛顿特区召开了国际子午线会议,商定将世界划分为24个标准时区,每个时区与通过英国格林尼治的本初子午线(0度经线)存在固定偏移。
在实际中,时区边界遵循政治边界,而非整齐的经线。中国横跨五个地理时区,却使用单一的官方时间(UTC+8)。印度使用UTC+5:30——半小时偏移。尼泊尔使用UTC+5:45。现实中的时区地图十分混乱。
UTC与GMT
GMT(格林尼治标准时间) 是格林尼治皇家天文台的平均太阳时。它作为世界时间基准已有一个多世纪。
UTC(协调世界时) 于1972年取代GMT成为国际标准。UTC基于原子钟而非天文观测,因此精度更高。在大多数实际用途中,UTC和GMT显示相同的时间,但UTC是正确的技术参考。
为什么是"UTC"而不是"CUT"? 这个缩写是英语"Coordinated Universal Time"(CUT)和法语"Temps Universel Coordonné"(TUC)之间的折中方案。双方都没有得到各自偏好的缩写,所以选择了UTC作为语言中立的替代方案。
夏令时:有组织的混乱
大约70个国家实行夏令时(DST),在春天将时钟拨快一小时,在秋天拨慢一小时。目的是让清醒时间与日照时间保持一致。结果是每年两次的Bug来源。
主要复杂之处:
- 并非普遍实行。 非洲、亚洲和南美洲的大部分地区不实行夏令时。在美国境内,亚利桑那州和夏威夷选择不实行。
- 日期不同。 欧盟在三月最后一个周日和十月最后一个周日调整。美国在三月的第二个周日和十一月的第一个周日调整。它们每年有好几周不同步。
- 时间歧义。 当时钟回拨时,凌晨1:00到2:00之间的那个小时会出现两次。那天的"1:30 AM"时间戳是有歧义的。
- 被跳过的时间。 当时钟前拨时,凌晨2:00到3:00之间的那个小时不存在。安排在那天凌晨2:30的会议永远不会发生。
- 政治变化。 政府可以(而且确实会)在短时间内更改夏令时规则。俄罗斯在2011年采用了永久夏令时,然后在2014年又切换为永久标准时间。摩洛哥已多次更改夏令时规则。
ISO 8601:通用日期格式
为避免歧义,国际标准ISO 8601定义了一种清晰的日期和时间格式:
2026-03-29T14:30:00Z
2026-03-29T14:30:00+02:00
2026-03-29T14:30:00-05:00
T分隔日期和时间。Z表示UTC(军事术语中的"Zulu"时区)。+02:00或-05:00是UTC偏移量。
这种格式无歧义、可按纯文本排序,且被所有日期解析库广泛支持。有疑问时,使用ISO 8601。
Unix时间戳
Unix时间戳(也称为纪元时间或POSIX时间)是自1970年1月1日 00:00:00 UTC(称为Unix纪元)以来经过的秒数。
| 人类可读格式 | Unix时间戳 |
|---|---|
| 1970-01-01 00:00:00 UTC | 0 |
| 2000-01-01 00:00:00 UTC | 946684800 |
| 2026-03-29 12:00:00 UTC | 1774987200 |
Unix时间戳没有时区——它们始终是UTC。这使得它们非常适合在软件中存储和比较时间。只在显示层转换为本地时区。
2038年问题: 以32位有符号整数存储Unix时间戳的系统将在2038年1月19日03:14:07 UTC溢出。最大值(2,147,483,647)会翻转为负数,被解释为1901年12月。大多数现代系统使用64位整数,在未来2920亿年内都不会溢出。
IANA时区数据库
软件不仅需要UTC偏移量——还需要了解每个地区的完整历史和未来规则,包括夏令时转换、政治变化和历史异常。这些信息存储在IANA时区数据库(也称为Olson数据库或 tzdata)中。
它使用 America/New_York、Europe/Paris、Asia/Tokyo 这样的标识符。每个条目编码了该地点的UTC偏移量和夏令时规则的完整历史。
这就是为什么你不应该将时区存储为固定偏移量(如"+02:00")。偏移量告诉你当前与UTC的差值,但不包含夏令时规则。Europe/Paris 冬季是UTC+1,夏季是UTC+2。IANA标识符可以同时捕获这两种情况。
软件中的常见Bug
- 存储没有时区的本地时间。 类似
2026-03-29 14:30:00的值,如果不知道它指的是哪个时区,就没有意义。始终存储UTC或包含偏移量。 - 假设UTC偏移量等于时区。 三月的UTC+2在七月可能变成UTC+3(如果该地区实行夏令时)。存储IANA标识符,而非偏移量。
- 在排程中忽略夏令时转换。 设在凌晨2:30的每日任务,如果不处理夏令时,每年会跳过一次,多运行一次。
- 假设一天有24小时。 在夏令时转换日,一天有23或25小时。通过加86,400秒来计算"明天同一时间"会有一小时的偏差。
- 简单地使用JavaScript
Date。new Date("2026-03-29")在某些引擎中被解析为UTC,在另一些中被解析为本地时间。始终明确指定时区。
开发者最佳实践
- 以UTC存储时间。 只在展示层转换为用户的本地时区。
- 使用IANA时区标识符(
America/New_York),而非固定偏移量(-05:00)。 - 使用ISO 8601进行序列化。 它无歧义且被广泛解析。
- 使用成熟的日期库。 JavaScript中使用
Intl.DateTimeFormat或date-fns-tz。Python中使用zoneinfo(3.9+)或pytz。Java中使用java.time.ZonedDateTime。 - 保持
tzdata更新。 政府会更改夏令时规则。你的操作系统和语言运行时需要最新的时区数据。 - 使用多个时区进行测试。 不要假设你的服务器和用户在同一个时区。
进阶学习
时间看似简单实则复杂,但规则都有详细文档,工具也很成熟。关键是尊重这种复杂性,而不是想当然地忽略它。
- 揭开Cron表达式的神秘面纱 — 跨时区的任务调度
- 哈希生成器 和 正则表达式测试器 — ToolK上更多的开发者工具
