关于时区的一些问题

一直以为 Asia/Shanghai 就等于 UTC+0800,直到最近遇到了个 bug,才发现它包含了 LMT、CDT、CST。

# Asia/ShanghaiEtc/GMT-8

类似于 Asia/Shanghai 这种时区表达形式,来自 IANA 的时区数据库 (opens new window), 用区域和地点来表示,记录了当地的时区变化(隐藏在时区里的历史)。

Etc 用作一些管理时区,例如 Etc/GMT-8 代表东八区,正负号与 ISO 8601 相反。

# 夏令时

为了节约能源,人为规定时间,搞了个夏令时(Daylight Saving Time:DST)出来,在春夏的时间调快一个小时。

1919 年,民国政府在上海和天津曾短暂地实行了一年夏令时,共和国在 1986 年至 1991 年实行了六年夏令时。 所以在这些时间段内,Asia/Shanghai的真正时区是 +0900 CDT,其他时候是 +0800 CST

在显示给用户的时候常常直接隐藏了时区信息,在夏令时交替的那一个小时就会有歧义。 比如 1991-09-15 01:00:00 可以是 CDT 也可以是 CST。

sh, _ := time.LoadLocation("Asia/Shanghai")
g8, _ := time.LoadLocation("Etc/GMT-8")

ti = time.Date(1919, 9, 15, 0, 0, 0, 0, sh)
fmt.Println(ti) // 1919-09-15 00:00:00 +0900 CDT

ti = time.Date(1991, 9, 15, 0, 0, 0, 0, sh)
fmt.Println(ti) // 1991-09-15 01:00:00 +0900 CDT

ti = time.Unix(ti.Unix(), 0).In(g8)
fmt.Println(ti) // 1991-09-15 00:00:00 +0800 +08

ti = time.Date(1991, 9, 15, 1, 0, 0, 0, sh)
fmt.Println(ti) // 1991-09-15 01:00:00 +0800 CST

ti = time.Unix(ti.Unix(), 0).In(g8)
fmt.Println(ti) // 1991-09-15 01:00:00 +0800 +08

ti, err := time.ParseInLocation("2006-01-02 15:04:05", "1991-09-15 01:00:00", sh)
fmt.Println(ti, err) // 1991-09-15 01:00:00 +0800 CST <nil>

# 地方平时 LMT

地方平时 LMT(Local Mean Time) 是太阳时改变形式修正后,在指定的经度范围内使用一致时间的地方太阳时。 从 19 世纪初期开始逐渐被采用,后被标准时间取代。
比如中国在 19 世纪末改为东经 120 度标准时,切换时刻为当地时间 1901 年元旦零点。

// +8:05:43
shLMT := time.FixedZone("SHLMT", 3600*8+60*5+43)

ti = time.Date(1901, 1, 1, 0, 0, 0, 0, shLMT)
fmt.Println(ti) // 1901-01-01 00:00:00 +0805 SHLMT

ti = time.Unix(ti.Unix(), 0).In(g8)
fmt.Println(ti) // 1900-12-31 23:54:17 +0800 +08

ti = time.Date(1900, 12, 31, 23, 54, 16, 0, sh)
fmt.Println(ti) // 1900-12-31 23:54:16 +0805 LMT

ti = time.Unix(ti.Unix(), 0).In(g8)
fmt.Println(ti) // 1900-12-31 23:48:33 +0800 +08

ti = time.Date(1900, 12, 31, 23, 54, 17, 0, sh)
fmt.Println(ti) // 1900-12-31 23:54:17 +0800 CST

ti = time.Unix(ti.Unix(), 0).In(g8)
fmt.Println(ti) // 1900-12-31 23:54:17 +0800 +08

# 避坑

任何需要隐藏时区信息的时候,就应该使用 Etc/GMT-8 来避免历史政治因素带来的坑。