Try Golang! time.Timeの等値判定で注意すること
Why isn’t the same time equal in Golang time.Time?
ふらっと遭遇したエラーに関連して、Goの time.Time
について調べてみたので、簡単にまとめてみます。まずは、下記のコードをご覧ください。
package mainimport (
"fmt"
"time"
)const tStr string = "2019-07-07T00:00:00+07:00"func main() {
t1, _ := time.Parse(time.RFC3339, tStr)
t2, _ := time.Parse(time.RFC3339, tStr) fmt.Printf("t1 == t2: %t\n", t1 == t2)
}// t1 == t2: false
time.Parse
メソッドを用いて同じ文字列から time.Time
のインスタンスをふたつ生成し、 ==
演算子で比較しています。わざわざブログで書くくらいなので、さすがに true
にはなりません。同じ時刻のはずなのに、なぜ true
にならないのでしょうか。今日はそこを順に見ていきます。
Goの “==”
演算子
Goの言語仕様にもある通り、 ==
演算子で構造体を比較する場合、全てのフィールドが等しければ、等しいと判定されます。 time.Time
は下記のように定義されているため、 wall
、 ext
、 loc
の全てが等しければ ==
演算子で比較した時に true
となります。
type Time struct {
wall uint64
ext int64
loc *Location
}
ここで注意したいのが、ポインタ型になっている loc
です。ポインタ型のフィールドについては、参照先の構造体ではなく、参照アドレスが等しいかどうかの判定になります。
Locationの参照アドレスを確認
func main() {
t1, _ := time.Parse(time.RFC3339, tStr)
t2, _ := time.Parse(time.RFC3339, tStr) fmt.Printf("t1.Location(): %p\n", t1.Location())
fmt.Printf("t2.Location(): %p\n", t2.Location())
}// t1.Location(): 0x434100
// t2.Location(): 0x434140
loc
の値を確認すると、参照アドレスが異なっていることが分かります。実は time.Parse
関数で time.Time
を生成する際、特定のロケーションを除いて、毎回新規でロケーションが作成されるため、参照アドレスが異なるのです。
ちなみに、タイムゾーンがUTCの場合は nil
が、タイムゾーンが実行環境のタイムゾーンと一致する場合はそのローカルロケーションとして用意された同一のインスタンスが毎回設定されるため、参照アドレスも一致します。
time.Timeの同値判定方法
Godocにも記載されている通り、 time
パッケージには Equal
メソッドが用意されているので、これを用いれば、タイムゾーンを考慮した時刻の比較ができます。しかし、マップのキーとして用いる場合などは、 Equal
メソッドは使用できません。このように、どうしても ==
演算子で比較したい場合はどうすれば良いでしょうか。
ひとつは、UTC(協定世界時)に変換しておくという方法です。上でも少し触れたように、UTCの場合は、必ず loc
の値が nil
になるようになっています。
func main() {
t1, _ := time.Parse(time.RFC3339, tStr)
t2, _ := time.Parse(time.RFC3339, tStr)
fmt.Printf("t1 == t2 : %t\n", t1 == t2)
fmt.Printf("t1.Equal(t2) : %t\n", t1.Equal(t2))
fmt.Printf("t1.UTC() == t2.UTC(): %t\n", t1.UTC() == t2.UTC())
}// t1 == t2 : false
// t1.Equal(t2) : true
// t1.UTC() == t2.UTC(): true
なお、上記の判定方法はいずれも「タイムゾーンを考慮して同じ時刻を同じとみなす」ものです。ロケーションの一致まで判定する必要があるのであれば、 reflect.DeepEqual
を使用したりと、要件に応じた判定方法を採用する必要があります。
Monotonic Clocksについて
time.Time
のフィールド loc
については上記で説明は終わりですが、残りの wall
と ext
は何ものなのでしょうか。実は、下記のように、時刻も loc
も等しいにも関わらず、 ==
演算子で比較すると false
となることがあります。これは wall
と ext
の値が異なるためですが、そのためにはMonotonic Clocksについて知っておく必要があります。
const fullfmt string = "2006-01-02T15:04:05.000000000Z07:00"func main() {
t1 := time.Now()
t2 := t1.Round(0) fmt.Printf("t1 : %s\n", t1.Format(fullfmt))
fmt.Printf("t2 : %s\n", t2.Format(fullfmt))
fmt.Printf("t1 == t2 (Loc): %t\n", t1.Location() == t2.Location())
fmt.Printf("t1 == t2 : %t\n", t1 == t2)
}// t1 : 2019-07-07T15:28:57.328585000+09:00
// t2 : 2019-07-07T15:28:57.328585000+09:00
// t1 == t2 (Loc): true
// t1 == t2 : false
Godocによると、プログラムの世界における「時間」には、時刻同期の影響を受ける時間(ここでは実時間とします)と、影響を受けない時間(Monotonic Clocks。ここでは単調時間とします。プロセス継続時間等)の2つがあり、 time.Time
パッケージでは、時間を「示す」際は実時間を、「測定する」場合は単調時間を用います。
time.Now
関数などでインスタンスを生成すると、実時間と単調時間の両方が time.Time
内に保持されており、時間を測定する場合は、たとえプログラムの実行中にOSの時刻が変更されても、正しく経過時間を測定できるように単調時間が使用されます。例えば、下記のようなコードでは、変数 elapsed
は、たとえOSの時刻が変更されようと、必ず約20秒の値となります。
start := time.Now()
// ... operation that takes 20 milliseconds ...
t := time.Now()
elapsed := t.Sub(start)
一方で、 time.Parse
関数などで生成した場合は単調時間は保持しません。また、単調時間を保持しているインスタンスについて、 Round(0)
とすることで、単調時間を取り除くことができます。実際の wall
と ext
の設定要領はやや複雑なのですが、同じ時刻でも単調時間の有無によって値が異なるため、上述の例では結果が false
となったのです。
ただ、実は UTC
メソッドなどでロケーションを変更すると、 Round(0)
と同様、単調時間が取り除かれるため、単純な ==
演算子での比較するためには、実際は UTC
だけで十分ということになります。
ざっくりですが、 time.Time
の同値判定について調べた内容をまとめてみました。Goのコードを読んでいると、なるほど、こういったことまで考慮されているのね、という気づきがあるので楽しいですね。