Bryan

Bryan

twitter
medium

談談時區

通常在本地化時往往會涉及到時區轉換的問題,而通常在真正關注到時區之前我們所「預設」使用的時區為 UTC 或「本地」。

本文以 Go 為例,分析下 Go 中的時區使用。

讀取時區#

在 Go 中,讀取時區使用的是 LoadLocation 函數。

// LoadLocation returns the Location with the given name.
//
// If the name is "" or "UTC", LoadLocation returns UTC.
// If the name is "Local", LoadLocation returns Local.
//
// Otherwise, 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
func LoadLocation(name string) (*Location, error)

閱讀註釋可知,如果 name 為空 / UTC 則使用 UTC、為 Local 則使用本地時區(在後面進行講解),否則,從特定位置進行讀取。

所謂讀取,是讀取的 tzfile 時區文件,可閱讀該文件查閱更多信息。簡單來說,時區文件是一個以 TZif 開頭的二進制文件,其中包含了時區的偏移量、閏秒、夏令時等信息,Go 可以讀取相關文件並解析。

  1. 如果存在 ZONEINFO 環境變量,利用該變量指向的目錄 / 壓縮文件進行讀取
  2. 在 Unix 系統上,使用系統標準位置
  3. (主要用於編譯 Go 時)從 $GOROOT/lib/time/zoneinfo.zip 進行讀取
  4. (如果 import 了 time/tzdata )從程式嵌入的資料讀取

我們比較關注的是 2,即 Unix 的標準時區文件的存儲位置。在 Unix 系統中,時區文件通常存儲在 /usr/share/zoneinfo/ 目錄中(根據系統不同,還可能是 /usr/share/lib/zoneinfo/ 或 /usr/lib/locale/TZ/),例如,中國(Asia/Shanghai)的時區定義文件就是 /usr/share/zoneinfo/Asia/Shanghai。因此,通常程式可以直接從系統中獲取到時區的信息。

注意,在 alpine 環境中,是沒有時區定義文件的,因此我們需要特別關注進行處理

  1. 可以在程式中使用 import _ "time/tzdata" 在編譯期將時區文件編入程式中,這樣在無法找到系統中的時區定義時也可以查找到標準的 IANA 時區定義
  2. 如果我們不需要特別動態的時區,我們可以避免使用 LoadLocation 而是使用 FixedZone 由我們自己提供時區名稱和偏移,例如對於中國 UTF+8 可以使用 time.FixedZone("Asia/Shanghai", 8*60*60)

本地時區#

通常在我們真正考慮到時區問題之前我們所「預設」使用的時區均為所謂的「本地時區」。

time.Now 為例,

type Time struct {
	wall uint64
	ext  int64

	loc *Location
}

// Now returns the current local time.
func Now() Time {
	sec, nsec, mono := now()
	mono -= startNano
	sec += unixToInternal - minWall
	if uint64(sec)>>33 != 0 {
		// Seconds field overflowed the 33 bits available when
		// storing a monotonic time. This will be true after
		// March 16, 2157.
		return Time{uint64(nsec), sec + minWall, Local}
	}
	return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}

可以看到,Time 結構體的最後一個字段 loc *Location 就是時區,而 time.Now 中使用的時區為 Local

我們本文主要關注時區,如果你對這段程式碼中的其他因素感興趣,歡迎閱讀 你真的了解 time.Now () 嗎?

這裡的 Local 就是本地時區,即執行這個程式所在的機器的時區。

// Local represents the system's local time zone.
// On Unix systems, Local consults the TZ environment
// variable to find the time zone to use. No TZ means
// use the system default /etc/localtime.
// TZ="" means use UTC.
// TZ="foo" means use file foo in the system timezone directory.
var Local *Location = &localLoc

閱讀 Go 中關於 Local 的說明可知,Go 會優先尊重 TZ 環境變量所指定的時區,如果沒有特殊指定,則使用 /etc/localtime 文件讀取當前時區。

那麼,Local 又是怎麼初始化的呢?

// localLoc is separate so that initLocal can initialize
// it even if a client has changed Local.
var localLoc Location
var localOnce sync.Once

func (l *Location) get() *Location {
	if l == nil {
		return &utcLoc
	}
	if l == &localLoc {
		localOnce.Do(initLocal)
	}
	return l
}

從這段程式碼的邏輯中不難出,Local 並沒有真的在程式啟動時讀取上述信息,而是在首次使用時才真正的通過執行 initLocal 函數來進行初始化。同時,這段程式碼也隱性的為使用 Location 提出了一個要求:必須呼叫 get 方法來獲取「真正的 Location」。

initLocal 函數在 zoneinfo_*.go 中定義,在不同的機器上有著不同的實現,但本質上都是

如果 TZ 內容以 : 開頭,則會忽略該冒號

  1. 如果沒有指定 TZ 環境變量,閱讀 /etc/localtime(通常就是指向了真正時區文件的軟連結)
  2. 如果指定的 TZ 環境變量為絕對路徑,閱讀該文件
  3. 否則按照上文所分析的 LoadLocation 流程進行時區文件的讀取

另外,上述 3 步驟如果失敗,會 fallback 到使用 UTC 時間

加餐:tzdata#

tzdata 詳細定義了歷史時區的變更情況,包括夏令時、閏秒等,因此 Asia/Shanghai 相比於簡單的 GMT+8 更具有通用性、且可正確處理歷史資料。

如果你感興趣,可以利用 zdump Asia/Shanghai -i 查看上海的時區變化,並和使用夏令時的時間 zdump America/Chicago -i 進行對比。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。