サマリ
- goのtimeパッケージの
IsZero()
はUnixTime = 0ではない - GAEのdatasotreのdefaultの時刻で
IsZero()
を使ってもtrueを返さない
IsZero()メソッドについて
refs: time package - time - pkg.go.dev
IsZero reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC.
IsZero()は 01-01 00:00:00 UTC
の時にtrueを返します。
ここで注意するべきはtrueを返す時刻はunixtimeのスタート時刻 1970-01-01 00:00:00 +0000 UTC
を指し示すわけではないということでです。
実際の挙動を見てみます。
def := time.Time{} fmt.Printf("%v\n", def) fmt.Printf("%v\n", def.IsZero()) // output // 0001-01-01 00:00:00 +0000 UTC // true
time.Time{}
は何もない時刻をinstance化する、すなわちtimeパッケージにおける標準時刻をinstance化することですが、これの結果は 0001-01-01 00:00:00 +0000 UTC
という時刻がinstance化され、IsZero() はこの時刻のときのみtrueを返します。
udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000") fmt.Printf("%v\n", udef) fmt.Printf("%v\n", udef.IsZero()) // output // 1970-01-01 00:00:00 +0000 +0000 // false
一方でコンピューターにおける時刻ゼロとはunixtimeの1番最初だと想起できるので、unixtimeのスタートした時刻に対して IsZero()
をcallすると、unixtimeのstartの時刻にもかかわらず false を返します。
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 }
とあり、そもそもの sec = 0
の時には January 1, year 1 00:00:00 UTC.
が初期値設定されています。
IsZero()
については
// IsZero reports whether t represents the zero time instant, // January 1, year 1, 00:00:00 UTC. func (t Time) IsZero() bool { return t.sec == 0 && t.nsec == 0 }
とあるので、そもそもunixtime=0を返さないのはgoのtimeパッケージの仕様のようです。
GAE上での挙動について
さて、ここで困ったのがGAEでDatastore上に time.Time
型で標準時刻をinstance化した時のことです。
以下のようなstructを考えてみます。
type App struct { ID int `datastore: "ID" json: "id"` CreatedAt time.Time `datastore: "createdAt" json: "created_at"` UpdatedAt time.Time `datastore: "updatedAt" json: "updated_at"` ReleasedAt time.Time `datastore: "ReleasedAt" json: "released_at"` }
この App
Entityがcreateされた時に CreatedAt
と UpdatedAt
はそれぞれcreateされた時刻が入りますが
リリースされたわけではないので、 ReleasedAt
には何も入りません。
つまり、 ReleasedAt
のfieldには time.Time{}
が入ってくることを期待してました。
しかし実際には 1970-01-01 00:00:00 +0900 JST
という日本標準時のでのunixtime = 0の状態が入っていました。
つまり、 ReleasedAt
に一度しか値を入れたくない、みたいな要件があったときに
if !app.ReleasedAt.UTC().IsZero() { // ReleasedAtにすでに値が入っている時 } else { // ReleasedAtに初回に値が入る }
上記のような条件分岐を考慮した場合、どんなときでも else 以下に入ってしまいます。
理由は上記で述べた通り、 unixtime のスタートはtimeパッケージで IsZero 判別するときには false を返してしまうからです。
ではどうすればいいかというと、実は unixtimeの最初の状態を作り出した time オブジェクトのunixtimeを取ると 0
になります。
udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000") fmt.Printf("%v\n", udef) fmt.Printf("%v\n", udef.UTC().Unix()) // output // 1970-01-01 00:00:00 +0000 +0000 // 0
これを利用して上記の条件分岐を以下のように書き換えます。
if app.ReleasedAt.UTC().Unix() != 0 { // ReleasedAtにすでに値が入っている時 } else { // ReleasedAtに初回に値が入る }
app.ReleasedAt.UTC().Unix()
とすることで、すでに ReleasedAt
に値が入ってきている場合は、 Unix()
でunixtimeに変換した時に 0以外
が入ってくる事になります。
まとめ
timeパッケージにおける IsZero()
の挙動とGAEのDatastoreでデフォルトの時刻を unixtime = 0
判定を同様に考えてきて、かなりハマりました。
IsZero()
がunixtimeのstart時刻を示さないのはどうにも納得が行きませんが、timeパッケージ的にはどうしようもなさそうなので、注意しようと思いました。
追記
このエントリを書いてから 4年以上経って言及されるとは思ってなかったですが、 いい感じに答えたが書いてあって参考にしたいなと思いました。
Goにはzero valuehttps://t.co/2NMmBdXz3M
— Hiroaki Nakamura (@hnakamur2) 2022年3月28日
という概念があってhttps://t.co/YUq3QgB5LX
でも数か所で言及されています。
で、time.IsZero()はtime.Timeがzero valueかどうかを返すんですよね。
Unix時間で0かは t.UnixNano() == 0 で判定すれば良いです。 https://t.co/UGr4fl45J8