为什么你不应该继续在文本格式中使用时间戳

这里是指,不要继续在人类可读的文本类传输格式(例如 JSON)中继续使用「时间戳」,而那些二进制格式,体积敏感的不在讨论范围内。

很长一段时间以来,我一直认为当大家提起「时间戳」这个名字时,大家都会无差别的理解为 UNIX time,即从 1970-01-01T00:00:00Z 到此时此刻的秒数,无视闰秒,每天都是准确的 86400 秒。
这个定义是标准化的,在 POSIX 标准和 Open Group Base Specification 中都有所涉及。

但近来,我发现真的有部分开发者对时间戳的理解存在差异,原因很简单,很多语言中提供的类时间戳表示都以毫秒为单位。比如在 Java 中,System.currentTimeMillis() 方法返回的是一个自 1970-01-01T00:00:00Z 至此时此刻的毫秒数;在 JavaScript 中,原生提供的 Date 类型,不管是构造器还是数值表示(指进行数值运算时进行的隐式转换)都以毫秒处理。
这导致每当我提起时间戳时,我不得不明确强调是 UNIX time,有时还要额外提醒,以秒为单位(总有些人不能立刻理解我强调 UNIX time 的目的)。

可读性?

放下关于时间戳标准的争论,我认为我们不得不回顾下,为什么要在文本类格式中使用时间戳。

实际上使用时间戳的缺点是不言自明的,首先,它的精度只到秒,如果你希望表达一个更精确的时间那就不得不做类似前文中提到的 Java 以及 JavaScript 中 API 的行为。

其次,人类不可读。我想我们之所以使用类似 JSON 这样的格式传输数据,可读性应该占据了比较大的比重,其次就是简单的格式兼容性更好一点。事实上 JSON 是一种设计得非常差的格式,不仅体积庞大,还难以人工编辑。而在 JSON 中使用时间戳呢,1693730046,这是写这篇文章时的时间,你能一眼告诉我这是何年何月吗?

难以人工编辑是相较于那些专为人工编辑的格式,比如 TOML 哪怕是新的 JSON5,都提供了忽略末尾逗号和注释的能力,这对解析器来说并不是什么难以做到的事

我就不提 JSON 没有对数值类型的精度给出明确定义,很多程序以双精度浮点数进行处理而导致的丢失精度问题了,即使是以毫秒表达的时间戳,要丢失精度也要等到 2255 年。(但请注意有些地方会以纳秒表达,这就要出问题了)一旦真的遇到这个问题,很多平台提供的不可扩展的 JSON 解析库将导致问题非常难以解决。

争论

总有些人从一些奇怪的角度以一些站不住脚的观点来反驳:

  • “我用时间戳,效率更高”

    在现代计算机上,都已经用 JSON 了,就不要再扣这一点效率了(不管是空间上的还是时间上的)。你有这个时间,不如考虑考虑要不要换成 Protobuf。

  • “我用时间戳,运算时只需要简单的在数值上加减即可”

    现代编程语言都提供了强大的日期时间函数库,你没有任何理由不去使用而是直接在一串数字上加加减减,不管是从编程的便利性上,还是工程的规范性上(使用一个原始的 Long 变量意味着失去类型系统的保护)。给你个时间戳,要一个月后的时间,你看着加吧!

    有人听不懂,我解释一下:我不是让你对字符串进行操作,是让你对 java.time.Instanttime.Time 进行操作。不要直接操作时间戳,更不能直接操作时间字符串。(有人想往后面堆个 Z 解决部分库默认输出 LocalDateTime 的问题,吓死人了)

    温馨提示:传输使用的格式,和程序内部如何处理时间是完全不相干的两回事,唯一的交点是传输时的序列化/反序列化

  • “我用时间戳,不需要考虑时区”

    这本来是我想强调的另一个问题,但考虑到文章的标题,我不得不限制讨论的范围,免得又臭又长。简而言之一句话,不用时间戳,只要你别作死,一样不需要考虑。

我相信对于一个有基本工程素养的工程师来说,这都是些伪问题。

能怎么办?

实际上,关于时间的标准已经有好几份了,从 ISO 8601RFC 3339。这些标准化的格式,不论是在可读性上,还是严谨程度上都不是盖的。从工程角度上讲,Java 中的 java.time.Instant java.time.OffsetDateTime,Go 中的 time.Time 默认的 JSON 序列化/反序列化格式都符合这些标准,大部分 SQL 数据库也可以无障碍输入输出;相反时间戳,或者某些人自创的格式反而有各种各样的问题。

ISO 8601RFC 3339 都是相对宽松的格式,它们都允许丰富的变体(数数标准中有多少 MAY),而很多函数库默认接受的输入输出都是其中的一个子集,最完整的子集。一般来说最好去使用明确列举在 RFC 3339 的 ABNF (事实上引用的 ISO 8601)的表示方法,这也是大部分函数库的默认输出格式(日期时间中间使用 ‘T’ 分隔)。

关于时区

这里要批评阿里巴巴的 Java 手册,可找到毒瘤源头了(笑),不赘述,写在 issue 里了,可见 alibaba/p3c#983

简单来说,你不能以“我没有跨时区业务,所以直接用某个时区就行了”来解释自己为什么不在时间表答中明确时区。一个 2023-09-03 17:00:00 这样的字符串摆出来,鬼知道到底是什么时区?你假设大部分人都按 UTC 理解,这假设根本不成立,无数人当作 UTC+8 处理,但却真的有好多软件会给你按不一定哪来的时区处理。加个时区,累不死人,也多占用不了多少硬盘、网络、CPU。

去掉时区的“当地时间”,只有在你明确的要表达是时区无关的“当地时间”时才可以使用。

总结一下,我是说请使用 2023-09-03T23:00:00+08:00 或者 2023-09-03T18:00:00Z 作为传输格式。