1. 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 方法提供了检验时间是否是显式初始化的一个简单途径

每一个Time都具有一个地点信息(即对应地点的时区信息)
当计算时间的表示格式时 如 Format Hour 和 Year 等方法 都会考虑该信息 Local UTC和 In 方法返回一个指定时区(但指向同一时间点)的 Time 修改地点/时区信息只是会改变其表示 不会修改被表示的时间点 因此也不会影响其计算

通过 == 比较 Time 时 Location 信息也会参与比较 因此 Time 不应该作为 map 的 key
 
1.1. 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 的最后一个字段表示地点时区信息 本章后面会专门介绍

1.2. 常用函数或方法
Time 相关的函数和方法较多 有些很容易理解 不赘述 查文档即可

1.2.1. 零值的判断
因为 Time 的零值是 sec 和 nsec 都是0 表示 1年1月1日
Time.IsZero() 函数用于判断 Time 表示的时间是否是 0 值

1.2.2. 与 Unix 时间戳的转换
相关函数或方法
time.Unix(sec, nsec int64) 通过 Unix 时间戳生成 time.Time 实例;
time.Time.Unix() 得到 Unix 时间戳;
time.Time.UnixNano() 得到 Unix 时间戳的纳秒表示;

1.2.3. 格式化和解析
这是实际开发中常用到的
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 这个包
格式化
时间格式化输出 使用 Format 方法 layout 参数和 Parse 的一样 Time.String() 方法使用了 2006-01-02 15:04:05.999999999 -0700 MST 这种 layout

1.2.4. 实现 序列化/反序列化 相关接口
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
}

1.2.5. 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)