通常在本地化时往往会涉及到时区转换的问题,而通常在真正关注到时区之前我们所「默认」使用的时区为 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 可以读取相关文件并解析。
- 如果存在
ZONEINFO
环境变量,利用该变量指向的目录 / 压缩文件进行读取 - 在 Unix 系统上,使用系统标准位置
- (主要用于编译 Go 时)从
$GOROOT/lib/time/zoneinfo.zip
进行读取 - (如果 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 环境中,是没有时区定义文件的,因此我们需要特别关注进行处理
- 可以在程序中使用
import _ "time/tzdata"
在编译期将时区文件编入程序中,这样在无法找到系统中的时区定义时也可以查找到标准的 IANA 时区定义 - 如果我们不需要特别动态的时区,我们可以避免使用
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 内容以
:
开头,则会忽略该冒号
- 如果没有指定 TZ 环境变量,阅读
/etc/localtime
(通常就是指向了真正时区文件的软链接) - 如果指定的 TZ 环境变量为绝对路径,阅读该文件
- 否则按照上文所分析的 LoadLocation 流程进行时区文件的读取
另外,上述 3 步骤如果失败,会 fallback 到使用 UTC 时间
加餐:tzdata#
tzdata 详细定义了历史时区的变更情况,包括夏令时、闰秒等,因此 Asia/Shanghai
相比于简单的 GMT+8
更具有通用性、且可正确处理历史数据。
如果你感兴趣,可以利用 zdump Asia/Shanghai -i
查看上海的时区变化,并和使用夏令时的时间 zdump America/Chicago -i
进行对比。