時間看起來很簡單,直到你嘗試撰寫正確處理時間的軟體。「下午 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 上更多的開發者工具
