reflect.DeepEqual 與 time.Time

Go 很像加強版的 C,例如只要把 type 想像成 function call 的第一個參數, 那 method 就只是自動放參數而已。比如 POSIX C 裡頭的 time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

NAME
time - get time in seconds

SYNOPSIS
#include <time.h>

time_t time(time_t *t);

DESCRIPTION
time() returns the time as the number of seconds since the
Epoch, 1970-01-01 00:00:00 +0000 (UTC).

If t is non-NULL, the return value is also stored in the memory
pointed to by t.

對照 Go 的 time.Unix():

1
2
3
4
func (t Time) Unix() int64

Unix returns t as a Unix time, the number of seconds elapsed since
January 1, 1970 UTC.

這樣就很明顯了。

會講到這個是因為這陣子碰到一個奇怪的 bug,相同的情況在 native 環境下 reflect.DeepEqual 是相等,而 Docker 環境下 DeepEqual 不相等。印 log 出來發現不相等原因是 struct 裡面 time.Time 這個 field 比出來不相等, 但用 %v 印出來卻是一樣的。參考 DeepEqual 的說明,推測應該是針對 Time 裡面的每個 field 用 == 去比對,所以就改用 %#v 印,抓到是 loc *Location 這欄位不一致,原因是 Docker 預設 timezone UTC,而我本機環境 是 CST。參考原始碼,time.Time 定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}

sec, nsec 這兩個欄位放的是 Epoch 絕對時間,而用 loc 放時區。所以 同樣的「時間點」,不同的「時區」時,會造成 struct 內容不一致, DeepEqual 就認為不相等了。追到這裡覺得似乎很合理,就把兩邊的時間都轉換 成 func (Time) Local 再來比對,結果卻仍然不相等,而且 %#v 印出來 「每個欄位」的值,包括 *Location 的 pointer 指向位址都一樣,這樣還是不 相等,就很傻眼了。

要再知道原因當然有辦法,讀 DeepEqual 的原始碼就可以,但已經花了不少時間, 而且這方向感覺解法不漂亮。因為 reflect 相關函數的速度都會慢,已經看這個 DeepEqual 不爽很久了,就直接另外用 type switches 寫了針對不同型別的比對 方式,碰到 time.Time 就直接呼叫 func (Time) Equal,把 DeepEqual 當 成最後手段來用。

然而這個疑惑還是沒完全解開,最近時間不夠用,只好先丟著以後再追。