本文主要是介绍Gopher 必知的时间知识,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 0.前言
- 1.时间基础概念
- 1.1 时间
- 格林尼治标准时间
- 世界时
- 国际原子时
- 协调世界时
- Unix
- 浮动时间
- 明确时间
- 1.2 时区
- 1.3 夏令时
- 2.Go 的时间
- 2.1 Time 结构
- 2.2 Duration 类型
- 2.3 Location 类型
- 2.4 time.now() 函数
- 3.时间转化
- 参考文献
0.前言
时间包括时间值和时区,没有包含时区信息的时间是不完整、有歧义的。
在与外部系统传递或解析时间时,应尽量采用无时区歧义的标准格式,如 HTTP 协议的时间格式或 Unix 时间戳。避免使用不包含时区信息的非标准时间格式(如 yyyy-mm-dd HH:MM:SS),因为这类格式在解析时可能会依赖默认的环境设置(如系统时区或数据库时区),从而引发潜在的问题。
1.时间基础概念
1.1 时间
格林尼治标准时间
GMT(Greenwich Mean Time)是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。自1924年2月5日开始,格林尼治天文台负责每隔一小时向全世界发放调时信息。格林尼治标准时间的正午是指当平太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。
由于地球每天的自转有些不规则,而且正在缓慢减速,因此格林尼治平时基于天文观测本身的缺陷,已经被原子钟报时的协调世界时(UTC)所取代。
世界时
世界时(Universal Time, UT)是一种基于地球自转的时间标准,用于描述全球的时间参考。
世界时是以地球自转为基准得到的时间尺度,其精度受到地球自转不均匀变化和极移的影响,为了解决这种影响,1955年国际天文联合会定义了 UT0、UT1 和 UT2 三个版本:
- UT0 是最基础的世界时定义,表示地球自转所产生的时间,但不考虑地球自转的不规则性和地球自转轴的偏移。
- UT1 是对 UT0 的修正,考虑了地球自转的不规则性(如地震、潮汐等引起的变化)。
- UT2 UT2 是 UT1 的进一步修正,考虑了季节性因素对地球自转速度的影响。UT2 用于精确的天文计算,但由于其对季节性变化的敏感性,应用较少。
国际原子时
TAI(International Atomic Time)是根据秒的定义(1秒为铯-133原子基态两个超精细能级间跃迁辐射振荡 9192631770 周所持续的时间)的一种国际参照时标,是均匀的时间尺度,与地球的空间位置不关联,起点为1958年1月1日0时0分0秒。随着时间的迁延,TAI 和 UT1 的时间差越来越大。为此,在 UT1 和 TAI 之间进行协调,这就产生了协调世界时。
协调世界时
UTC(Coordinated Universal Time)又称世界统一时间,是最主要的世界时间标准。基于国际原子时,并通过不规则地加入闰秒来抵消地球自转变慢的影响。闰秒在必要的时候会被插入到 UTC 中,以保证协调世界时(UTC)与世界时(UT1)相差不超过 0.9 秒。
Unix
它是UNIX或类UNIX系统使用的时间表示方式。一般定义为从协调世界时(UTC时间)1970年1月1日0时0分0秒起至现在的总秒数(10位是精确到秒,13位是精确到毫秒)。考虑到闰秒的话,更精确的定义为从协调世界时(UTC时间)1970年1月1日0时0分0秒起至现在经过闰秒调整之后的总秒数。
浮动时间
浮动时间(Floating Time)和浮动日期/时间是与特定时区无关的时间值。例如你的生日。
明确时间
明确时间(Explicit Time)或具有时区偏移量信息,它表示具体的某一个时刻,像 Go 的time.now()。
拓展阅读:
关于时间术语的解释可以参考香港天文台官网。
各种时间的时间差参见 GPS, UTC, and TAI Clocks。
从一九七二年起加入闰秒的记录,参见香港天文台官网的记录:
https://www.hko.gov.hk/sc/gts/time/Historicalleapseconds.htm
1.2 时区
时区是指地球上的某一个区域使用同一个时间定义。主要为了每个地方太阳直射的时候接近中午12点。从太阳升起到太阳落下,时刻从0到24变化。这样,不同经度的地方时间自然会不相同。为了解决这个问题,人们把地球按经度划分为不同的区域,每个区域内使用同一个时间定义,相邻的区域时间差为1个小时。
时区又分为理论时区和法定时区:
- 理论时区:按经度,每15°为一个时区,将地球划分为24个时区,以本初子午线为中心,向东西两侧各延伸7.5°的区域为0时区。
- 法定时区:法定时区是在理论时区的基础上,根据某些地区的国界线做了调整之后的时区。为实际使用的时区。例如中国横跨东五区到东九区五个时区,但统一使用东八区时间(北京时间)。
- 时差:某个地方的时刻与0时区的时刻差称为时差,时差东正西负。以本初子午线为中心,每向东一跨过一个时区,时刻增加一个小时,每向西跨过一个时区,时刻减少一个小时。
- 国际日期变更线:大体以180度经线为日界线。当自西向东穿过日期变更线时,日期需要减少一天,反之,日期增加一天。
世界各时区时间参见:
https://www.hko.gov.hk/sc/gts/time/clock/clockR.htm?menu=time
1.3 夏令时
它是为节约能源而人为规定地方时间的制度。一般在天亮早的夏季人为将时间提前一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。
在施行夏令时的国家,一年里面有一天只有23小时(夏令时开始那一天),有一天有25小时(夏令时结束那一天),其他时间每天都是24小时。
实施夏令时的国家:
开发需要注意的是:
- 有些地区执行夏令时,会在夏令时拨快一小时,具体变化时间由所在地政策决定,北美洲则是在02:00调快时间。是不会有02:00:00~02:59:59这个时间段的,设置定时任务时一定要注意避免这个时间点处理业务,否则会不执行。
- 在结束执行夏令时当天,是会重复一次 01:00:00~01:59:59 这个时间段,设置定时任务时一定要注意可重入。
- 在实行夏令时的国家,如遇到夏令时的开始与结束时,那么今天与昨天同一个时刻相差不是恒等于24小时,有可能是23或25小时,所以不能加减 (24 x 3600) 秒作为日期的往前往后的计算依据。
2.Go 的时间
2.1 Time 结构
先介绍两个概念:
- wall time: 就是挂在墙上的时钟,我们在计算机中能看到的当前时间就是 wall time,比如’2020-10-10 10:00:00’
- monotonic time: 从字面意思是单调递增的时间,从os启动开始从0计数,重启后重新开始从0开始,单调递增。
按照我们对时间的理解,只需要wall time就够了,为什么还需要monotonic time,难道wall time不是单调递增的。
我们来看一起事故。CloudFlare的CDN服务中断。从文中事故描述来看,Now() 两次读取当前时间,计算时间差: 后一次Now - 前一次Now,计算结果为负值,然后 CloudFlare DNS 的代码没有考虑这种情况,然后程序就出错了。
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.wall uint64ext int64// loc specifies the Location that should be used to// determine the minute, hour, month, day, and year// that correspond to this Time.// The nil location means UTC.// All UTC times are represented with loc==nil, never loc==&utcLoc.loc *Location
}
// 1.9以后版本
Go 官方定义 Time 结构代表具有纳秒精度的瞬间。A Time represents an instant in time with nanosecond precision。这个和所在地域或者所处时区是无关的。
从源码注释和源码我们看到wall和ext的存储结构分为两种情况:
- 第一种情况 hasMonotonic = 1
hasMonotonic 标志位存储在wall字段的最高位,为1,wall的64~31位存储从1985-01-01 00:00:00到当前时间的秒数,wall的1-30位存储当前时间的纳秒数。ext字段存储:进程启动到当前时间点的纳秒数.不同的系统获取该值的方式不同,它是系统的调用的返回值. 这个值就是monotonic time,独立于 wall time,单调递增。
- 第二种情况 hasMonotonic = 0
hasMonotonic 标志位存储在wall字段的最高位,为 0,顾名思义,就是 wall 和 ext 中不包含monotonic time。wall的64~31位永远为0,wall的1-30位存储当前时间的纳秒数。ext字段存储从 01-01-01 00:00:00 到当前时间的秒数。
这种设计主要是为了能解决上面说到的"时间倒流"的问题。
当hasMonotonic = 1这种存储模式下,两次now函数的返回值会使用ext 字段相减来完成,而ext存储着monotonic time,是单调递增的,所以不会出现两次now()相减出现负数的情况,也就解决了时间倒流的情况。
2.2 Duration 类型
Duration 类型用于表示两个时刻 ( Time ) 之间经过的时间,以纳秒 ( ns ) 为单位。
1 纳秒等于一秒的十亿分之一,即等于 10 的负 9 次方秒 ( 1 ns = 10(-9) s)
Duration 类型的定义type Duration int64
。
2.3 Location 类型
time 包中只有两个时区变量 time.Local 和 time.UTC。
time.now() 默认的时区变量 Local 即本地时区,取决于运行的系统环境设置,优先取 TZ 这个环境变量,然后取 /etc/localtime,都取不到就用 UTC 兜底。
云厂商申请的容器环境 TZ 一般是空,而 /etc/localtime 是 CST+8。如腾讯云不会根据地域设置不同的时区,美分和日本也是东八区。
其他时区变量有两种方法获取,一个是通过 time.LoadLocation 函数根据时区名字加载, 时区名字见 IANA Time Zone database, LoadLocation首先查找系统zoneinfo, 然后查找$GOROOT/lib/time/zoneinfo.zip
。
另一个是在知道时区名字和偏移量的情况下直接调用 time.FixedZone(“$zonename”, $offsetSecond) 构造一个Location对象。
再来看看Location的结构,这里的Location应该翻译为位置比较好。
一个 位置(Location) 包含多个 时区(Zone) 和多个 tx 时区变迁(Transition)。
因为同一个国家或地区,随着时间的推移,政权的更替和国家政策的改变,时区的定义可能会发生改变。
zone 记录了这个位置所有出现过的时区,而 tx 则记录了一个时区是何时变迁到另一个时区的。
// A Location maps time instants to the zone in use at that time.
// Typically, the Location represents the collection of time offsets
// in use in a geographical area. For many Locations the time offset varies
// depending on whether daylight savings time is in use at the time instant.
type Location struct {name stringzone []zonetx []zoneTrans// ... 省略 ...
}// A zone represents a single time zone such as CET.
type zone struct {name string // abbreviated name, "CET" 时区名称,比如 CET 、UTC 、CEST 、GMT 等offset int // seconds east of UTC 相对于 UTC 0 的时间偏移,比如我们北京位于东八区,那么就是 8 * 36400 = 291200isDST bool // is this zone Daylight Savings Time? 是否为夏时令
}// A zoneTrans represents a single time zone transition.
type zoneTrans struct {when int64 // transition time, in seconds since 1970 GMTindex uint8 // the index of the zone that goes into effect at that timeisstd, isutc bool // ignored - no idea what these mean
}
在格式化时间方法 Time.Format → Time.AppendFormat → Time.locabs → Location.lookup
lookup是在干嘛? 它在查找输入的参数 sec(Unix 时间戳) 该使用此位置的哪个时区。通过二分查找,找到当前时间所属的tx区间,然后取zone[tx.Index]作为时区配置。
// Binary search for entry with largest time <= sec.
// Not using sort.Search to avoid dependencies.
tx := l.tx
end = omega
lo := 0
hi := len(tx)
for hi-lo > 1 {m := lo + (hi-lo)/2lim := tx[m].whenif sec < lim {end = limhi = m} else {lo = m}
}
zone := &l.zone[tx[lo].index]
name = zone.name
offset = zone.offset
start = tx[lo].when
// end = maintained during the search
isDST = zone.isDST
2.4 time.now() 函数
Go 语言中的 time.Now() 函数是通过调用操作系统提供的接口来获取当前时间的。具体来说,它调用了底层的 C 函数 gettimeofday() 或者 clock_gettime() 来获取当前系统时间。
在 Unix 系统中,gettimeofday() 和 clock_gettime() 都是由操作系统内核提供的函数,可以获得从某个固定起点开始到现在所经过的秒数和微秒数或者纳秒数。在 Windows 系统中,time.Now() 则调用了 Win32 API 的 GetSystemTimeAsFileTime() 函数来获取当前时间。
time.now()依赖于系统时间。
容器共享了宿主机操作系统内核的时间。而我们宿主机的系统时间是怎么同步的?
我们部署应用程序的服务器上,都会启动一个「自动校准」时间的服务,这个服务就是 NTP(Network Time Protocol),它可以保证每台机器的时间与时间服务器保持同步。
NTP 如何同步时间?
NTP 最典型的授时方式是Client/Server方式。如下图所示,客户机首先向服务器发送一个NTP 包,其中包含了该包离开客户机的时间戳T1,当服务器接收到该包时,依次填入包到达的时间戳T2、包离开的时间戳T3,然后立即把包返回给客户机。客户机在接收到响应包时,记录包返回的时间戳T4。客户机用上述4个时间参数就能够计算出2个关键参数:NTP包的往返延迟d和客户机与服务器之间的时钟偏差 t。客户机使用时钟偏差来调整本地时钟,以使其时间与服务器时间一致。
T1为客户发送NTP请求时间戳(以客户时间为参照);T2为服务器收到NTP请求时间戳(以服务器时间为参照);T3为服务器回复NTP请求时间戳(以服务器时间为参照);T4为客户收到NTP回复包时间戳(以客户时间为参照);d1为NTP请求包传送延时,d2为NTP回复包传送延时;t为服务器和客户端之间的时间偏差,d 为NTP包的往返时间。
现已经T1、T2、T3、T4,希望求得t以调整客户方时钟:
式1:
假设NPT请求和回复包传送延时相等,即d1=d2,则可解得:
式2:
根据式(1),t也可表示为:t=(T2-T1)+d1=(T2-T1)+d/2…式(3)
可以看出,t、d只与T2、T1差值及T3、T4差值相关,而与T2、T3差值无关,即最终的结果与服务器处理请求所需的时间无关。因此,客户端即可通过T1、T2、T3、T4计算出时差t去调整本地时钟。
3.时间转化
大部分时候前端输入的日期时间字符串形式没有携带时区。
只有下面这些都处理对,程序才不会有bug。
- 将其正确存储在数据库或缓存中。
- 在存储与应用程序之间进行了正确的转换。
- 应用程序与客户端和前端(如浏览器或操作系统)之间的正确转换。
Go 中有如下时间的转换:
(1)解析时间 string 转 Time。
- time.Parse() 默认以时区 UTC+0 的形式转化
- time.ParseInLocation() 转成指定时区的时间
(2)格式化时间 Time 转 string。
- time.Format(layout) 格式化时间。
layout 格式可以和 time.String() 和 json.Marshal() 一样指定时区如:
Format("2006-01-02 15:04:05.999999999 -0700 MST")
如果 $layout 没有指定显示时区,那么字符串只有时间没有时区,时区信息丢失了。
- time.In(timeLocation).Format() 指定时区格式化。
参考文献
time package
正确的处理时间(基于“数据的存储和显示相分离”的设计原则) - 虚极静笃 - 博客园
这篇关于Gopher 必知的时间知识的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!