Go中微妙的数据损失:打印的时间戳没有排序的具体实例

120 阅读4分钟

当我写到高性能数据丢失时,许多情况是相当明显的。你不遵守一个标准,你就会被咬伤。这里是另一个不遵守的例子,它可以以相当意外的方式咬你。

在计算机中保持时间是非常棘手的,它充满了例外情况、变化、角落案例,它只是很难正确实施。但如果做得好,你会得到回报.例如,当你把时间打印成字符串时,特别是如果是ISO-8601或RFC3339,你会得到一个非常好的好处,这些字符串是有序的。这样,你就知道2020-07-21 是在2020-02-131970-01-05 之后,而且你不用解析这些字符串就知道了。

为什么这很重要?有两个原因。

  1. 解析时间和日期是很昂贵的。它可能看起来不是这样的,但试着解析数千或数百万的日期,你就会发现。
  2. 在某些情况下,解析是不合适的。例如,文件列表,许多文件系统或对象存储给你的文件是有排序的,如果你在每个文件名前加上一个日期,你可以确保它们也是按时间排序的。

现在来谈谈我们手头的问题。如果你使用Go和它的time 包,它将以最微妙的方式偏离标准:它以毫厘/微米/纳秒为单位截断尾部的零,所以2020-07-21T08:21:00.629280Z 变成2020-07-21T08:21:00.62928Z 。这似乎不是什么大问题,但它破坏了我们原来的排序保证。

这意味着,如果你把你的文件名前置,或者如果你有存储为${time}.csv 的对象,你不能只是调用一个列表函数并按排序顺序处理数据,你需要加载所有的文件名,解析其中的日期,对它们进行排序,然后处理数据。否则你就会丢失数据,因为你依赖于它们的排序,而它们并没有。

这里有一段代码来重现这个问题。

package main

import (
	"fmt"
	"log"
	"sort"
	"time"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	n := 100

	times := make([]string, 0, n)
	// now := time.Now().UTC()
	now, err := time.Parse("2006-01-02", "2020-07-21")
	if err != nil {
		return err
	}

	for j := 0; j < n; j++ {
		now = now.Add(time.Microsecond)
		newTime := now.Format(time.RFC3339Nano)
		times = append(times, newTime)
	}
	sort.Strings(times)

	for j := 1; j < len(times); j++ {
		t1, t2 := times[j-1], times[j]
		t1p, err := time.Parse(time.RFC3339, t1)
		if err != nil {
			return err
		}
		t2p, err := time.Parse(time.RFC3339, t2)
		if err != nil {
			return err
		}

		if t1p.UnixNano() > t2p.UnixNano() {
			fmt.Printf("apparently, %s precedes %s\n", t1, t2)
		}

	}

	return nil
}

这就产生了

apparently, 2020-07-21T00:00:00.000019Z precedes 2020-07-21T00:00:00.00001Z
apparently, 2020-07-21T00:00:00.000029Z precedes 2020-07-21T00:00:00.00002Z
apparently, 2020-07-21T00:00:00.000039Z precedes 2020-07-21T00:00:00.00003Z
apparently, 2020-07-21T00:00:00.000049Z precedes 2020-07-21T00:00:00.00004Z
apparently, 2020-07-21T00:00:00.000059Z precedes 2020-07-21T00:00:00.00005Z
apparently, 2020-07-21T00:00:00.000069Z precedes 2020-07-21T00:00:00.00006Z
apparently, 2020-07-21T00:00:00.000079Z precedes 2020-07-21T00:00:00.00007Z
apparently, 2020-07-21T00:00:00.000089Z precedes 2020-07-21T00:00:00.00008Z
apparently, 2020-07-21T00:00:00.000099Z precedes 2020-07-21T00:00:00.00009Z

如果你打印被排序的字符串片断的受影响部分,你会得到这个系列

2020-07-21T00:00:00.000015Z
2020-07-21T00:00:00.000016Z
2020-07-21T00:00:00.000017Z
2020-07-21T00:00:00.000018Z
2020-07-21T00:00:00.000019Z
2020-07-21T00:00:00.00001Z    <- this precedes all the timestamps above
2020-07-21T00:00:00.000021Z
2020-07-21T00:00:00.000022Z
2020-07-21T00:00:00.000023Z
2020-07-21T00:00:00.000024Z

在这一点上,你不能依赖这些格式化的字符串被排序,也就是说,除非你使用你自己的格式化字符串。Go中的RFC3339Nano ,不能依赖,在其排序保证方面不能依赖。