DevKitLab Logo DevKitLab
时区 / UTC / ISO 8601 / 日期与时间

UTC、GMT、ISO 8601 与 Unix 时间戳有什么区别?

UTC、GMT、结尾的 Z、+08:00、一串十位数字,常被当成同一个东西混用,而时间的 bug 正是这么溜进来的。它们不是「时间」的几种变体,而是处在四个不同的层。这篇把参考时间尺度、时区、偏移,以及写下一个瞬间的两种记法各自归位。

你总是遇见同一个时刻,只是每次换了身衣服。同事说「统一都存 UTC 就好」。某个 API 甩给你 "2026-07-16T09:00:00Z"。一个配置文件要的是 GMT+8。数据库里某一列存着 1700000000。一封日历邀请写着 2026-07-16T17:00:00+08:00。它们看起来都在说「时间」,于是把它们当成可以互换的东西似乎很安全——把带 Z 的那个当 GMT 读、把日历字符串后面的偏移随手删掉、把 API 返回的值不加转换就塞进时间戳列。然后某处就偏了八小时,或者明年春天一场会议落到了错误的日期,而谁也看不出为什么。

有一句重新表述能让这一切对齐:这五样不是同一个东西的五个名字。它们处在四个不同的层,各自回答不同的问题。一个是参考尺度——所有时钟都以它为准的那条线。一个是时区——一个有名字的地区,它的规则决定了本地时钟离那条线多远、以及什么时候变。一个是偏移——那套规则在某个具体瞬间算出来的、固定的 +HH:MM。还有两个是记法——把同一个瞬间写下来的方式,一个写成文本、一个写成数字。把它们搞混,不是一个无关痛痒的口误,而是把量具、量度规则和量出来的结果混为一谈。

这篇文章要把这一摞按层理清。UTC 到底是什么(一个参考尺度,不是一个地方)。为什么 GMT 有两种含义、既不该替 UTC、也不该替「英国时间」。以及那个引发最多真实 bug 的区别——像 Asia/Shanghai 这样的时区,和它今天恰好算出的偏移 +08:00,是两回事;还有藏在这道缝里的 GMT+8Etc/GMT+8 陷阱。再往后是 ISO 8601 是什么(一种写法,不是一个时间)、它的字符串到底什么时候才真能按时间排序、以及那个 Z 意味着什么。最后是它们如何拼合,好让「存成 UTC」「给我一个 ISO 字符串」和「1700000000」不再互相较劲,而是从不同角度描述同一个瞬间。它是《Unix 时间戳为什么不对》的姊妹篇——那篇讲那个数字,这篇讲围绕它的这一堆名字。

唯一的核心:四个层,不是五个同义词

把这些词按四种职责归位,困惑就消散了:

  • 参考尺度——UTC。一条大家约定好的线,其他所有时钟都相对它来定义。它不是一个地方,也没有谁的挂钟非得显示它。
  • 时区——Asia/ShanghaiEurope/London。一个有名字的地区,带着一整套规则的历史:它的各种偏移、它的夏令时切换,以及这些规则过去的每一次变动。这些都存在 IANA 时区数据库里。
  • 偏移——+08:00-05:00Z。相对 UTC 的一个固定小时数,针对某一个瞬间——是一个时区的规则为某个时刻算出来的值,而不是时区本身。
  • 记法——ISO 8601 文本和 Unix 时间戳数字。把同一个瞬间钉下来的两种方式:ISO 8601 把它写成人能读的文本,Unix/POSIX 时间戳把它数成一个数字。带 Z 或数值偏移的 ISO 8601 字符串,以及 Unix/POSIX 时间戳,都可以把一个瞬间锚定到 UTC。

所以:只有一个参考(UTC),带着规则的时区,那套规则在某个瞬间算出的偏移,以及一个瞬间的两种拼写。GMT 是唯一没有自己一层的词——正如下面会看到的,它在其中两层之间漂移。

UTC:参考尺度,不是你生活其中的时区

UTC——协调世界时——是一个参考时间尺度,不是一个时区。它是全世界约定用来度量时间的那条主线,由全球一组原子钟共同维护,并偶尔用一个闰秒微调,好让它永远不至于和地球的实际自转差太远。地球上每一个民用时间,都被定义为 UTC 加减某个偏移。(这个缩写故意既不是英文语序也不是法文语序——是一个委员会的折中,好让哪种语言都不「独占」它。)

对日常代码来说,有两个后果要紧:

  • 没人非得显示 UTC,但所有人都能认同。东京挂钟上是 UTC+9,芝加哥是 UTC−6 或 −5,而这两者是同一个瞬间、以同一个参考表达出来的结果。这正是「已确定的瞬间存 UTC」是好建议的原因:它是两台服务器、或两个用户永远不会各执一词的那个读数。(注意「已确定」二字——未来的一场本地约定是个例外,后面会讲为什么。)
  • UTC 从不实行夏令时。它是一条固定的参考,不会往前拨。夏令时是时区叠加上去的东西。所以当一个存好的 UTC 时间「一到夏天就偏了一小时」,动的不是 UTC,而是一次时区转换。

有一点当下的背景值得一提,毕竟现在是 2026 年:闰秒制度本身就要变了。2022 年,国际计量大会决定在 2035 年或之前提高 UT1−UTC(UTC 与地球自转之间的差距)的允许上限,预计这将不再需要按当前规则频繁插入闰秒。所以「偶尔的闰秒」是一套已被安排要变的机制,而不是宇宙的永恒定律。无论如何要抛弃的误解是:把 UTC 当成「伦敦时间」或「GMT」。它是那些挂钟被度量所依据的那条线,而不是其中某一块。

GMT:一个标签,两种含义

GMT——格林尼治标准时间——不是 UTC 的干净同义词,因为它有两种含义。

  1. 历史上,一种时间尺度:格林尼治子午线上的平均太阳时,系于地球的自转(与今天所说的 UT1 密切相关)。从 1880 年代起它是全世界的参考,直到 1970 年代由原子钟定义的 UTC 接手。
  2. 在今天的日常用法里,一个零偏移标签:UTC+00:00 的替身。当有人说「14:00 GMT」,他几乎总是指 UTC,两者相差远不到一秒——在你不关心亚秒精度时,足够近了。

对日志和随意协调,把「GMT」当 UTC 读无伤大雅。但要守住两条护栏:

  • 要一个参考去锚定存下来的数据时,说 UTC,而不是 GMT——UTC 是精确的现代尺度,GMT 是个含糊的词,有时指历史尺度、有时指零偏移标签。
  • 「GMT」并不全年等于「英国时间」。英国冬天用 GMT,夏天却切换到 BST(英国夏令时,UTC+1)。所以在代码里,绝不要把 GMT 硬编成「英国」——用 IANA 时区 Europe/London,它知道 BST 的存在;GMT 这个标签从不移动,移动的是伦敦的挂钟。

最容易出错的区别:时区不是偏移

这个区别值得牢牢记住,因为它是那些悄无声息、最终溜进生产环境的时间 bug 的根源。Asia/Shanghai+08:00 不是同一件事的两种说法——它们是两个不同的层。

  • 时区——Asia/ShanghaiEurope/BerlinAmerica/New_York——是一个有名字的地区,带着一整套规则的历史:它当前的偏移、是否以及何时实行夏令时,还有这些规则过去的每一次变动。它们存在随你的操作系统和语言运行时一起分发的 IANA 时区数据库里——而这个数据库每年会更新好几次,常常是因为某国政府用政治决定改了自己的时钟,所以一个时区的规则并非一成不变。
  • 偏移——+08:00-05:00Z——是一个时区的规则在某个瞬间算出来的固定结果。它告诉你那个时刻在 UTC 之前或之后多远,除此之外什么都不说。

为什么这个区别不是空谈:偏移没法告诉你在另一个瞬间偏移会是多少。柏林冬天是 +01:00,夏天是 +02:00。把一个未来事件存成「明年三月,9:00 +01:00」,你就把今天的偏移冻结到了一个可能落在春季夏令时切换之后的日期上——于是这场会议会提前一小时发生。要正确地存一个未来的本地事件,你必须保留时区名字加上本地时间(Europe/Berlin09:00),这样偏移是按那时生效的规则算出来的。(时区还会让某些挂钟时间在夏令时切换时消失或重复——春天向前拨时那个根本没发生的小时、秋天向后拨时那个发生两次的小时——这本身就是一类「差一小时」的 bug,姊妹篇走过那些情形。)

这道缝里藏着三个陷阱,而且三个都真实存在:

  • GMT+8+08:00Etc/GMT+8 不是同一个字符串+08:00 是 ISO/RFC 的偏移(比 UTC 早八小时,即 UTC+8)。GMT+8 是某些库接受的非正式写法。但 IANA/POSIX 数据库里的 Etc/GMT+8 符号是反的——它表示 UTC−8,而不是 +8。想用 Etc/GMT+8 表示上海,你会落到太平洋去。跨系统传输优先用 +08:00;需要规则时,用 Asia/Shanghai 这样真正的时区。
  • 时区缩写有歧义——永远别去解析它们CST 既是美国中部时间,也是中国标准时间,还是古巴标准时间。IST 是印度、以色列、还有爱尔兰。它们不带任何可靠的偏移;把它们当成纯展示的装饰,存的是 IANA 名字。

当你需要看一个瞬间落在好几个有名字的时区里分别是什么样——每个时区真正的规则和夏令时都被应用——这正是时区转换器的用途:它会解析时区,而不是假设一个被冻结的偏移。

ISO 8601:一种写法,不是一个时间

ISO 8601 是日期和时间的一种文本格式——一种记法,不是时钟本身。它存在的意义,是让一个写下来的时刻在不同语言和地区之间都无歧义,而不必玩 03/04/05 那种猜谜游戏。它的形状是:

2026-07-16T09:00:00Z          ← 日期、「T」、时间,再加一个 UTC 标志
2026-07-16T17:00:00+08:00     ← 同一个瞬间,改用偏移来写

要紧的部件:日期是大端YYYY-MM-DD),一个字面的 T 把日期和时间分开,而人们最容易忘、却最重要的部件,是结尾那个标志——要么是 Z,要么是像 ±HH:MM 这样的偏移——它才是把文本系到参考上的那根绳。没有它,这个字符串就只是一个没有锚点的本地日期时间——应用必须自己补上缺失的时区,而解析器可能退回到某个默认时区(往往是运行环境的本地时区),也可能干脆拒绝。(那种悄悄退回本地时间的行为,恰好就是姊妹篇里那个「差一天」的 bug。)

这里有一句半真半假的话值得纠正:「ISO 字符串按纯文本排序就是按时间排序」。确实如此——但前提是它们先被归一化:同一种格式、同样的精度、补零对齐,而且同一个偏移,最好都先转成 UTC 的 Z。一旦混用偏移,文本顺序就不再等于时间顺序——2026-07-16T20:00:00+08:00 按文本排在 2026-07-16T13:00:00Z 之后,可它却是更早的瞬间(12:00Z 对 13:00Z)。所以「可排序」这个超能力是真的,但它是归一化后的 UTC 字符串的性质,而不是 ISO 文本本身普遍具备的。

再有两个补充说明,能省下真实的困惑。第一,当一个 API 说「发一个 ISO 8601 时间戳」,它几乎总是指 RFC 3339,也就是 ISO 8601 更严格的互联网子集——完整的日期和时间,外加一个必需的偏移(Z 或数值)。而单纯的 ISO 8601 还允许一些你在 API 里很少想要的东西,比如周日期(2026-W29)和时长(P3DT4H)。第二,Z+00:00 完全相等:两者都说「偏移为零——这是 UTC」。

「Z」与「Zulu」:最小的那块拼图

...09:00:00Z 结尾那个 Z,不过是零偏移、也就是 UTC 的标志。在航空和军事里念出声,它是「Zulu」——字母 Z 在通话拼读字母表里的词——所以你会听到有人把一个瞬间叫作「0900 Zulu」。Z+00:00 是写零偏移的两种标准方式;「UTC」只是对这个含义的文字说明,不是能加在时间戳末尾的后缀(2026-07-16T09:00:00UTC 并不是合法格式)。三者指向同一个锚点,但只有前两个是语法。唯一的陷阱是忘了Z 却又默认它——一个本该带上它、却没带的字符串不是 UTC,而是有歧义的。

那个数字:Unix(POSIX)时间戳

存着 1700000000 的那一列,是另一种记法——把瞬间写成一个计数,而不是文本。它的规则值得说清楚,因为对「时间戳数字」的含糊说法,正是位数混淆的起点:

  • 这个计数从 Unix 纪元起点 1970-01-01T00:00:00Z 开始——和其他一切一样,锚定在 UTC 上。
  • POSIX 时间以秒计,而且它有意忽略闰秒(每天都当成恰好 86,400 秒),这让这个数字能用简单的算术可逆地还原成日期。
  • 单位并不通用。1700000000(今天是 10 位);JavaScript 的 Date.now() 返回的是毫秒1700000000000,13 位);有些系统用微秒或纳秒。把秒交给一个想要毫秒的东西,正是姊妹篇围绕展开的那个经典 bug。
1700000000       Unix/POSIX 秒
1700000000000    Unix 毫秒(JavaScript)

想在这个计数和人类可读的日期之间来回——以及判断一个值是秒还是毫秒——Unix 时间戳转换器负责的正是这一段。

它们究竟如何拼合

把一个瞬间放进它们全部走一遍,各层就不再较劲:

瞬间:         一个确定的时刻
时间戳:        1700000000                    (秒,从 UTC 参考起算)
ISO 8601(Z):2023-11-14T22:13:20Z          (文本,锚定在 UTC)
ISO 8601(+):2023-11-14T14:13:20-08:00     (同一个瞬间;那天洛杉矶算出的偏移)
时区:         「Asia/Shanghai」→ 挂钟上是次日 06:13

每一行都是同一个时刻。时间戳数字和那两个 ISO 字符串,是它的三种记法;时区的规则算出偏移,把它转回某人的挂钟;UTC 是它们全都倚靠的那个参考。这就是为什么那套分层的建议是自洽而非自相矛盾的:「存成 UTC」意思是让瞬间锚定在参考上——一个时间戳数字或一个 Z 字符串都能做到;「给我一个 ISO 字符串」要的是那种文本记法;「转成用户的时区」只在显示时发生一次,用的是时区的规则。

把决策收在一处

大多数真实问题归根结底都是该存什么。下面这张表覆盖了其中几乎所有情况(而整数时间戳与 ISO 字符串之间到底怎么选、原生列类型又如何取舍,另有一篇专门细讲):

你要存的东西就存这个
一个已经发生的时刻(created_at、日志、审计记录)一个 UTC 瞬间——Unix 时间戳,或以 Z 结尾的 ISO 8601 字符串
任何固定的时间点那个 UTC 瞬间(时间戳数字或 Z 字符串)
一个未来的本地事件(「明年三月柏林的 9:00」)本地日期时间 加上 IANA 时区名Europe/Berlin)——绝不用光秃秃的偏移
一个周期性日历事件本地时间 + IANA 时区 + 重复规则
要展示给用户看的东西不用新存什么——在最后一步把存好的 UTC 瞬间转成查看者的时区

有两个习惯能让这张表立得住。第一,「统一都存 UTC」几乎对,但太笼统了已确定的瞬间存 UTC,但未来的本地日程要存本地时间加一个 IANA 时区,否则一次规则变动会悄悄把它们挪走。第二,当这些词开始模糊时,问问自己身处哪一层——参考、时区、偏移,还是记法——答案通常就自己冒出来了。

贯穿始终的那条线,回到数字本身,可以这样概括:UTC 是参考尺度;时区带着规则;偏移是那套规则在某个瞬间算出的结果;而时间戳计数和 ISO 字符串,是把那个瞬间写下来的两种方式。一旦每个词都有了自己的层,「UTC 还是 GMT 还是 ISO 8601」就根本不再是一道二选一——它们从来就没在回答同一个问题。

参考规范