# 4.3 Time 类型详解 #
`Time` 代表一个纳秒精度的时间点。
程序中应使用 Time 类型值来保存和传递时间,而不是指针。就是说,表示时间的变量和字段,应为time.Time类型,而不是*time.Time.类型。一个Time类型值可以被多个go程同时使用。时间点可以使用Before、After和Equal方法进行比较。Sub方法让两个时间点相减,生成一个Duration类型值(代表时间段)。Add方法给一个时间点加上一个时间段,生成一个新的Time类型时间点。
Time 零值代表时间点 January 1, year 1, 00:00:00.000000000 UTC。因为本时间点一般不会出现在使用中,IsZero 方法提供了检验时间是否是显式初始化的一个简单途径。
每一个时间都具有一个地点信息(及对应地点的时区信息),当计算时间的表示格式时,如Format、Hour和Year等方法,都会考虑该信息。Local、UTC和In方法返回一个指定时区(但指向同一时间点)的Time。修改地点/时区信息只是会改变其表示;不会修改被表示的时间点,因此也不会影响其计算。
通过 == 比较 Time 时,Location 信息也会参与比较,因此 Time 不应该作为 map 的 key。
## Time 的内部结构
```
type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// Only the zero Time has a nil Location.
// In that case it is interpreted to mean UTC.
loc *Location
}
```
要讲解 `time.Time` 的内部结构,得先看 `time.Now()` 函数。
```
// Now returns the current local time.
func Now() Time {
sec, nsec := now()
return Time{sec + unixToInternal, nsec, Local}
}
```
now() 的具体实现在 `runtime` 包中,以 linux/amd64 为例,在 sys_linux_amd64.s 中的 `time·now`,这是汇编实现的:
* 调用系统调用 `clock_gettime` 获取时钟值(这是 POSIX 时钟)。其中 clockid_t 时钟类型是 CLOCK_REALTIME,也就是可设定的系统级实时时钟。得到的是 struct timespec 类型。(可以到纳秒)
* 如果 `clock_gettime` 不存在,则使用精度差些的系统调用 `gettimeofday`。得到的是 struct timeval 类型。(最多到微妙)
*注意:* 这里使用了 Linux 的 vdso 特性,不了解的,可以查阅相关知识。
虽然 `timespec` 和 `timeval` 不一样,但结构类似。因为 `now()` 函数返回两个值:sec(秒)和 nsec(纳秒),所以,`time·now` 的实现将这两个结构转为需要的返回值。需要注意的是,Linux 系统调用返回的 sec(秒) 是 Unix 时间戳,也就是从 1970-1-1 算起的。
回到 `time.Now()` 的实现,现在我们得到了 sec 和 nsec,从 `Time{sec + unixToInternal, nsec, Local}` 这句可以看出,Time 结构的 sec 并非 Unix 时间戳,实际上,加上的 `unixToInternal` 是 1-1-1 到 1970-1-1 经历的秒数。也就是 `Time` 中的 sec 是从 1-1-1 算起的秒数,而不是 Unix 时间戳。
`Time` 的最后一个字段表示地点时区信息。本章后面会专门介绍。
## 常用函数或方法
`Time` 相关的函数和方法较多,有些很容易理解,不赘述,查文档即可。
### 零值的判断
因为 `Time` 的零值是 sec 和 nsec 都是0,表示 1年1月1日。
Time.IsZero() 函数用于判断 Time 表示的时间是否是 0 值。
### 与 Unix 时间戳的转换
相关函数或方法:
* time.Unix(sec, nsec int64) 通过 Unix 时间戳生成 `time.Time` 实例;
* time.Time.Unix() 得到 Unix 时间戳;
* time.Time.UnixNano() 得到 Unix 时间戳的纳秒表示;
### 格式化和解析
这是实际开发中常用到的。
* time.Parse 和 time.ParseInLocation
* time.Time.Format
#### 解析
对于解析,要特别注意时区问题,否则很容易出 bug。比如:
```
t, _ := time.Parse("2006-01-02 15:04:05", "2016-06-13 09:14:00")
fmt.Println(time.Now().Sub(t).Hours())
```
`2016-06-13 09:14:00` 这个时间可能是参数传递过来的。这段代码的结果跟预期的不一样。
原因是 `time.Now()` 的时区是 `time.Local`,而 `time.Parse` 解析出来的时区却是 time.UTC(可以通过 `Time.Location()` 函数知道是哪个时区)。在中国,它们相差 8 小时。
所以,一般的,我们应该总是使用 `time.ParseInLocation` 来解析时间,并给第三个参数传递 `time.Local`。
#### 为什么是 2006-01-02 15:04:05
可能你已经注意到:`2006-01-02 15:04:05` 这个字符串了。没错,这是固定写法,类似于其他语言中 `Y-m-d H:i:s` 等。为什么采用这种形式?又为什么是这个时间点而不是其他时间点?
* 官方说,使用具体的时间,比使用 `Y-m-d H:i:s` 更容易理解和记忆;这么一说还真是~
* 而选择这个时间点,也是出于好记的考虑,官方的例子:`Mon Jan 2 15:04:05 MST 2006`,另一种形式 `01/02 03:04:05PM '06 -0700`,对应是 1、2、3、4、5、6、7;常见的格式:`2006-01-02 15:04:05`,很好记:2006年1月2日3点4分5秒~
*如果你是 PHPer,喜欢 PHP 的格式,可以试试 [times](https://github.com/polaris1119/times) 这个包。*
#### 格式化
时间格式化输出,使用 `Format` 方法,`layout` 参数和 `Parse` 的一样。`Time.String()` 方法使用了 `2006-01-02 15:04:05.999999999 -0700 MST` 这种 `layout`。
### 实现 序列化/反序列化 相关接口
`Time` 实现了 `encoding` 包中的 `BinaryMarshaler`、`BinaryUnmarshaler`、`TextMarshaler` 和 `TextUnmarshaler` 接口;`encoding/json` 包中的 `Marshaler` 和 `Unmarshaler` 接口。
它还实现了 `gob` 包中的 `GobEncoder` 和 `GobDecoder` 接口。
对于文本序列化/反序列化,通过 `Parse` 和 `Format` 实现;对于二进制序列化,需要单独实现。
对于 json,使用的是 `time.RFC3339Nano` 这种格式。通常程序中不使用这种格式。解决办法是定义自己的类型。如:
```
type OftenTime time.Time
func (self OftenTime) MarshalJSON() ([]byte, error) {
t := time.Time(self)
if y := t.Year(); y < 0 || y >= 10000 {
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
}
// 注意 `"2006-01-02 15:04:05"`。因为是 JSON,双引号不能少
return []byte(t.Format(`"2006-01-02 15:04:05"`)), nil
}
```
### Round 和 Truncate 方法
比如,有这么个需求:获取当前时间整点的 `Time` 实例。例如,当前时间是 15:54:23,需要的是 15:00:00。我们可以这么做:
```
t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:00:00"), time.Local)
fmt.Println(t)
```
实际上,`time` 包给我们提供了专门的方法,功能更强大,性能也更好,这就是 `Round` 和 `Trunate`,它们区别,一个是取最接近的,一个是向下取整。
使用示例:
```
t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2016-06-13 15:34:39", time.Local)
// 整点(向下取整)
fmt.Println(t.Truncate(1 * time.Hour))
// 整点(最接近)
fmt.Println(t.Round(1 * time.Hour))
// 整分(向下取整)
fmt.Println(t.Truncate(1 * time.Minute))
// 整分(最接近)
fmt.Println(t.Round(1 * time.Minute))
t2, _ := time.ParseInLocation("2006-01-02 15:04:05", t.Format("2006-01-02 15:00:00"), time.Local)
fmt.Println(t2)
```
# 导航 #
- 上一节:[时区](04.2.md)
- 下一节:[定时器](04.4.md)
- 简介
- 第一章 输入输出 (Input/Output)
- 1.1 io — 基本的 IO 接口
- 1.2 ioutil — 方便的 IO 操作函数集
- 1.3 fmt — 格式化 IO
- 1.4 bufio — 缓存 IO
- 第二章 文本
- 2.1 strings — 字符串操作
- 2.2 bytes — byte slice 便利操作
- 2.3 strconv — 字符串和基本数据类型之间转换
- 2.4 regexp — 正则表达式
- 2.5 unicode — Unicode 码点、UTF-8/16 编码
- 第三章 数据结构与算法
- 3.1 sort — 排序算法
- 3.2 index/suffixarray — 后缀数组实现子字符串查询
- 3.3 container — 容器数据类型:heap、list 和 ring
- 第四章 日期与时间
- 4.1 主要类型概述
- 4.2 时区
- 4.3 Time类型详解
- 4.4 定时器
- 第六章 文件系统
- 6.1 os — 平台无关的操作系统功能实现
- 6.2 path/filepath — 操作路径
- 第七章 数据持久存储与交换
- 7.1 database/sql — SQL/SQL-Like 数据库操作接口
- 第八章 数据压缩与归档
- 8.1 flate * DEFLATE 压缩算法
- 第九章 测试
- 9.1 testing * 单元测试
- 9.2 testing * 基准测试
- 9.3 testing * 子测试
- 9.4 testing * 运行并验证示例
- 9.5 testing * 其他功能
- 9.6 httptest * HTTP 测试辅助工具
- 9.7 总结
- 第十章 进程、线程与 goroutine
- 10.1 创建进程
- 10.2 进程属性和控制
- 10.3 线程
- 10.4 进程间通信
- 第十三章 应用构建 与 debug
- 13.1 flag * 命令行参数解析
- 13.2 log * 日志记录
- 13.3 expvar * 公共变量的标准化接口
- 13.4 runtime/debug * 运行时的调试工具