
1.3 便捷的时间工具
在查看原始数据时,经常需要看格式化后的个性化时间,或是时间戳等。如果不同系统中的时间格式不一样,比较规则不一样,那么每用一次就要做一轮转换。很多时候,业务接口的入参开始时间和结束时间是一个时间戳的值,这时就需要依靠外部的一些快捷站点,或是内部的Web站点来获取、调整时间了。首先要连上网,然后输入站点地址,等等。这显然不符合我们的极客思维,因此本节就做一个时间相关的工具,提高获取时间的效率。
1.3.1 获取时间
在项目的internal目录下新建timer目录,并新建time.go文件,目录结构如下:

在time.go文件中写入如下代码:

在GetNowTime方法中对标准库time的Now方法进行封装,用于返回当前本地时间的Time对象。此处的封装主要是为了便于后续对Time对象做进一步的统一处理。
1.3.2 推算时间
下面对时间进行推算,在time.go文件中新增方法,代码如下:

在上述代码中,我们调用了两个方法来处理,分别是 ParseDuration 方法和 Add 方法。ParseDuration方法用于从字符串中解析出duration(持续时间),其支持的有效单位有ns、us (或µs)、ms、s、m和h,例如,"300ms","-1.5h" or "2h45m"。而在Add方法中,我们可以传入其返回的duration,这样就可以得到当前Timer时间加上duration后所得到的最终时间。
为什么要添加一个ParseDuration方法,而不直接用Add方法来做呢?实际上,在这个时间工具中,我们预先并不知道传入的值是什么,因此最好用ParseDuration方法先处理一下。
如果预先知道准确的duration,且不需要适配,那么即可直接使用Add 方法进行处理,代码如下:

1.3.3 初始化子命令
在处理完获取时间和推算时间后,我们需要将其集成到子命令中,即创建项目的time子命令。在项目的cmd目录下新建time.go文件,新增如下代码:


在编写完time子命令后,再到项目的cmd/root.go文件中进行相应的注册即可:

每一个子命令都需要到rootCmd中进行注册,否则无法使用。
1.time now子命令
如果需要获取当前时间,则在time子命令下新增一个now子命令,用于处理具体的逻辑。在time.go文件中新增如下代码:

在获取当前时间的Time对象后,一共输出了两种格式的时间,分别如下:
(1)第一种格式:通过调用Format方法输出按既定的2006-01-02 15:04:05格式进行格式化的时间。
(2)第二种格式:通过调用Unix方法返回UNIX时间,即时间戳,其值为自UTC 1970年1月1日起经过的秒数。
如果想要定义其他时间格式,则可以使用标准库time,它支持(内部预定义)的格式如下:


使用预定义格式的代码如下:

2.time calc子命令
如果需要推算时间,则在 time 子命令下新增一个 calc 子命令,用于处理具体的逻辑。在time.go文件中新增如下代码:

在上述代码中,一共展示了三种常用时间格式的处理,分别是:时间戳、2006-01-02 和2006-01-02 15:04:05。
在时间格式处理上,我们调用strings.Contains方法对空格进行包含判断。若存在空格,则按既定的2006-01-02 15:04:05格式进行格式化,否则以2006-01-02格式进行处理。若出现异常错误,则直接按时间戳格式进行转换处理。
最后,我们对time命令的now、calc子命令和其相关联的命令行参数进行注册,代码如下:

1.3.4 验证
在开发完相关功能后,需对这些功能进行验证。下述命令分别获取了当前时间,以及推算的所传入时间的后五分钟和前两小时:

需要注意的是,这里的时间是虚构的时间,读者可根据本地的实际输出时间进行验证。
1.3.5 时区问题
如果在验证命令时,没有遇到“少了八小时”之类的问题,则说明是相对顺利的。但这里面有一个隐藏问题,即可能会忽略“时区问题”这个“坑”。实际上,在使用标准库time时是存在遇到时区问题这个风险的,因此需要特别注意。下面介绍如何解决“时区问题”。
一般来说,不同的国家(有时甚至是同一个国家内的不同地区)使用不同的时区。对于需要输入和输出时间的程序来说,必须要考虑系统所处的时区。在Go语言中,Location用来表示地区相关的时区,一个Location可能表示多个时区。
在标准库time中,提供了Location的两个时区:Local和UTC。Local表示当前系统本地时区;UTC表示通用协调时间,也就是零时区。标准库time默认使用的是UTC时区。
1.Local 是如何表示本地时区的
时区信息既浩繁又多变,UNIX 系统以标准格式存于文件中。这些文件位于/usr/share/zoneinfo中,而本地时区可以通过/etc/localtime获取。这是一个符号链接,指向/usr/share/zoneinfo中的某一个时区。例如,笔者本地电脑指向的是/var/db/timezone/zoneinfo/Asia/Shanghai。
在初始化Local时,标准库time通过读取/etc/localtime即可获取系统的本地时区,代码如下:

2.设置时区
我们可以通过标准库time中的LoadLocation方法根据名称获取特定时区的Location实例,原型如下:

在该方法中,如果传入的name是UTC或为空,则返回UTC;如果传入的name是Local,则返回当前的本地时区Local;否则name应该是IANA时区数据库(IANA Time Zone Database,简称tzdata)中记录的地点名(该数据库记录了地点和对应的时区),如"America/New_York"。
另外需要注意的是,LoadLocation 方法所需要的时区数据库可能不是所有系统都有提供,特别是在非UNIX系统中,LoadLocation方法会查找环境变量ZONEINFO指定的目录或解压缩该变量指定的zip文件(如果有该环境变量);然后查找UNIX系统约定的时区数据安装位置。如果都找不到,就会查找$GOROOT/lib/time/zoneinfo.zip中的时区数据库。简单来说,就是在不同的约定路径中尽可能地查找所需的时区数据库。
为了保证获取的时间与期望的时区一致,我们需要修改获取时间的代码,设置当前时区为Asia/Shanghai,修改如下:

3.需要注意的time.Parse/Format
在前面的代码中,我们用到了time.Format方法,与此相对应的time.Parse方法并没有介绍。Parse方法会解析格式化的字符串并返回它表示的时间值,十分常见,并且有一个非常需要注意的点。我们一起看看下面这个示例程序,代码如下:

这个示例程序的输出结果是什么呢?会是2029-09-04 12:02:33吗?下面一起来看看最终的输出结果:
输入时间:2029-09-04 12:02:33,输出时间:2029-09-04 20:02:33
从输出结果来看,输入时间和输出时间竟然相差了八个小时,这显然是时区的设置问题。在调用Format方法前我们已经设置了时区,为什么还会出现时区问题呢?
实际上这与Parse方法有直接关系,因为Parse方法会尝试在入参的参数中分析并读取时区信息。如果入参的参数没有指定时区信息,那么会默认使用UTC时间。因此在这种情况下,我们采用ParseInLocation方法指定时区,就可以解决这个问题,代码如下:

也就是说,所有解析与格式化的操作都最好指定时区信息,否则当项目已经上线,并且遇到了时区问题时,再进行数据清洗就比较麻烦了。
4.我的系统时区是对的
我们常常会说,“程序运行在我的本地是正常的……”这个经典答复。实际上,我们在开发时,用的可能是本地或预装好的开发环境,时区往往都是设置正确(符合我们东八区的需求)的。我们可以在本地查看localtime文件,命令如下:

可以发现,实际上输出的就是 CST-8,即中国标准时间,UTC+8,因此不设置时区也不会出现异常,但是到了其他部署环境就不一定了。举个例子,在Kubernetes、Docker盛行的今天,我们编译后的Go程序很可能运行在Docker中,假设该镜像没有经过时区调整,我们在编译和启动时也没有指定时区,那么就会遇到很多问题。比如日志的写入时间不对、标准库time的转换存在问题,等等。
因此我们需要确保所有部署环境的系统时区是正确的。
这样就万无一失了吗?实际上,当部署的环境并不存在所设置时区的时区数据库时,也会导致回滚到UTC时区,因此我们必须与对接的运维人员共同确保部署时区的各方面设置都是正确的。
1.3.6 参考时间的格式
2006-01-02 15:04:05是一个参考时间的格式,如同其他语言中的Y-m-d H:i:s格式,其功能是用于格式化处理时间。
为什么要用2006-01-02 15:04:05呢,其实这些“数字”是有意义的。在Go语言中,强调必须显示参考时间的格式,因此每个布局字符串都是一个时间戳,而并非随便写的时间点。如果觉得记忆困难,则可参见官方例子中的如下方式:

而对于2006-01-02 15:04:05,则可以将其记忆为2006年1月2日3点4分5秒。
1.3.7 小结
在Go语言中,很多刚入门的小伙伴都会遇到标准库time的各类问题,尤其是时区设置、格式化时间、2006-01-02 15:04:05这样的问题,更是遇到一个,踩一个“坑”。
因此在本节,我们在基于标准库time完成时间工具的需求基础之上,还做了进一步的说明,争取让读者在面对这类问题时,能知其然,更知其所以然。