不喜欢广告? 无广告 今天

时区只是一个谎言(以及在代码中如何处理时区)

发布日期

时区在地图上看起来很简单。实际上并非如此。以下是它们如何破坏软件——以及在代码中处理时区的正确思维模型。

时区是谎言(以及如何在代码中处理它们)1
广告 移除?

你打开一张地图,看到垂直切片。“UTC-5是东部时间,UTC+1是中欧时间。”简单吧?

错误。时区是政治建构,而不是地理事实——这个区别将帮助你避免软件开发中一些最糟糕的bug。

时区实际上为何如此混乱

以下是你操作系统时区数据库在背后默默为你处理的内容:

  • 夏令时(DST) ——并非所有国家都实行夏令时,各国规则不同,且切换日期因地理位置而异。美国在2007年更改了日期。埃及在2011年废除夏令时,之后又恢复,再废除。
  • 半小时和四分之一个小时偏移 ——印度(UTC+5:30)、尼泊尔(UTC+5:45)以及澳大利亚部分地区(UTC+9:30)存在。你认为偏移都是整数小时的假设是错误的。
  • 历史变更 ——俄罗斯在2014年从UTC+4变更为UTC+3。萨摩亚在2011年12月30日跳过了一整天,以跨越国际日期变更线。当你处理历史时间戳时,“正确”的偏移可能与今天完全不同。
  • 模糊时间 ——当时钟倒退时,凌晨1:30会出现两次;当时钟前进时,凌晨2:30完全不存在。如果你存储“2024年11月3日早上1:45东部时间”而没有UTC偏移,你就创建了一个模糊的时间戳。

这些都不是小细节。每一个边缘情况都曾导致真实生产环境中的bug——错过日历事件、重复收费、调度系统崩溃。

解决大多数问题的唯一规则

以UTC存储和处理时间,仅在显示时转换为本地时间。

这并非有争议的建议,而是所有主要数据库、API标准和分布式系统团队的共识。如果你的数据库列存储 2024-11-03 06:45:00 在UTC,你始终能确切知道它代表的是哪个时刻——无论服务器位于何处、用户处于哪个时区,或当天是否实行夏令时。

一旦你存储 2024-11-03 01:45:00 而没有UTC偏移,你就失去了信息。你创建了一个表示两个不同含义的时间戳。

在JavaScript中如何处理时区

JavaScript内置的 Date 对象将时间存储为自1970年1月1日UTC以来的毫秒数——这很好。问题在于其API令人困惑且不一致。

应避免的内容

// BAD: Parsing without explicit timezone
const d = new Date('2024-11-03 01:45:00');
// Interpreted as LOCAL time — behavior varies by environment

// BAD: Storing user-facing strings as dates
const meeting = '3:00 PM Thursday';
// This is meaningless without a timezone

应使用的内容

// GOOD: Always use ISO 8601 with explicit UTC offset
const d = new Date('2024-11-03T06:45:00Z'); // Z = UTC

// GOOD: Display in user's local timezone using Intl API
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'full',
  timeStyle: 'short',
});
console.log(formatter.format(d)); // "Sunday, November 3, 2024 at 1:45 AM"

如果你正在构建超出简单日期格式化的功能,请使用 Luxon 库。它拥有完整的时区支持,正确处理夏令时转换,并清晰表达代码意图。

import { DateTime } from 'luxon';

// Parse an ISO string and convert to a specific zone
const utc = DateTime.fromISO('2024-11-03T06:45:00Z');
const eastern = utc.setZone('America/New_York');
console.log(eastern.toLocaleString(DateTime.DATETIME_FULL));
// "November 3, 2024, 1:45 AM EDT"

在Python中如何处理时区

Python 的 datetime 模块区分了 朴素 (无时区) 和 有意识 (有时区) 的datetime对象。朴素的datetime是陷阱——在跨越时区边界时的任何代码中都应避免使用。

from datetime import datetime, timezone
import zoneinfo  # Python 3.9+

# BAD: naive datetime (no timezone info)
d = datetime(2024, 11, 3, 1, 45, 0)

# GOOD: always attach a timezone
d_utc = datetime(2024, 11, 3, 6, 45, 0, tzinfo=timezone.utc)

# Convert to a specific timezone
eastern = zoneinfo.ZoneInfo('America/New_York')
d_eastern = d_utc.astimezone(eastern)
print(d_eastern)  # 2024-11-03 01:45:00-05:00

对于较旧版本的Python或更复杂的调度需求, python-dateutil箭头 都是经过良好维护的选择。核心原则始终不变:在UTC中工作,仅在边界处转换。

在数据库中如何处理时区

数据库时区处理甚至会让经验丰富的开发者出错。

PostgreSQL

PostgreSQL有两种时间戳类型: timestamp (无时区) 和 timestamptz (带时区)。尽管名称如此, timestamptz 实际上并不存储时区——写入时转换为UTC,读取时转换回会话时区。这是正确的行为。始终使用 timestamptz.

-- BAD
CREATE TABLE events (created_at TIMESTAMP);

-- GOOD
CREATE TABLE events (created_at TIMESTAMPTZ);

-- Querying across timezones
SELECT created_at AT TIME ZONE 'America/New_York' FROM events;

MySQL

MySQL 的 DATETIME 存储无时区上下文的时间戳——你输入什么,就得到什么,没有转换。 TIMESTAMP 转换写入时为UTC,读取时转换回当前服务器时区——但仅限于1970年至2038年之间的日期(Unix时间戳溢出)。实际上:使用 DATETIME 列,并确保你的应用程序始终显式写入UTC值。

跨时区调度:隐藏的陷阱

假设用户安排了一个每周一次的会议,时间为“每周一上午9点纽约时间”。你将下一个发生时间存储为UTC时间戳。没问题——直到夏令时变更,此时你存储的UTC时间对应的是纽约时间的上午10点,而非上午9点。

解决方案:不要为重复事件存储绝对的未来时间戳。存储 本地规则 ——时区标识符加上本地时间——并在运行时计算下一个UTC时间戳。这样,系统在夏令时变更后仍能正确重新计算。

-- Store the intent, not the absolute moment
CREATE TABLE recurring_events (
  id SERIAL PRIMARY KEY,
  timezone TEXT NOT NULL,       -- 'America/New_York'
  local_time TIME NOT NULL,     -- '09:00:00'
  recurrence TEXT NOT NULL      -- 'WEEKLY:MONDAY'
);
-- Compute next_occurrence_utc in application code at dispatch time

IANA时区名称与UTC偏移

当存储用户时区偏好时,应存储IANA名称(America/New_York, Europe/London, Asia/Kolkata)——而不是UTC偏移(UTC-5, UTC+1)。偏移是快照;IANA名称编码了完整的夏令时历史和未来规则。

America/New_YorkUTC-5 冬季和 UTC-4 夏季。如果你存储 UTC-5,你就会在一年中一半时间嵌入错误的偏移。

快速参考:规则

  • 将时间戳存储为UTC ——在数据库或API响应中,永远不要例外。
  • 使用IANA时区名称 ——而不是UTC偏移——用于用户偏好和重复调度。
  • 永远不要在没有明确时区的情况下解析日期字符串 ——隐式行为依赖于环境。
  • 仅在显示层转换为本地时间 ——而不是在业务逻辑或数据库查询中。
  • 对于重复调度,存储规则 ——而不是预计算的UTC时间——以确保夏令时变更不会破坏未来的事件。
  • 用边缘情况测试 ——夏令时转换小时、接近国际日期变更线的日期,以及历史上变更过偏移的地区的时间戳。
想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?