Goのtime.Nowとは? 〜synctestを添えて〜 はじめに エブリーでエンジニアをやっております、 赤川 です。食事管理アプリ ヘルシカ の開発を通じてGoを嗜んでいます。 ダイエット・食事管理・体重管理・カロリー計算 - ヘルシカ every, Inc. ヘルスケア/フィットネス 無料 ふと、以下のコードを見て、「Goにおける現在時刻ってなんなんだ…?」となりました。 now := time.Now() OSから取って来ているのは既知とした上で、Goのコードでそれをどのような形で扱っているのか、synctestの仮想時刻を返す挙動がどのように実装されているのかなど、いろいろ気になったのでコードを追っていこうと思います。 本記事で話すこと Goの時刻の扱い方 Goの現在時刻の取得の実装(ある程度高レイヤーの部分のみ) 本記事で話さないこと 他プログラミング言語の現在時刻の取得方法との違い プラットフォーム別実装など低レイヤーの詳細 time.Now の戻り値の作られ方 ウォールクロックとモノトニッククロック まず、OSが提供する時刻ソースには、大きく2種類あります。 種類 内容 特徴 ウォールクロック 現実で扱われている時刻。OSはUNIXエポック(1970-01-01 UTC)からの経過秒数として返すことが多い NTP補正・サマータイム・手動変更で 過去に巻き戻ることがある モノトニッククロック マシン起動などを起点とした、単調増加するカウンタ 必ず単調増加。日付としての意味は持たない これらの扱いについて、 time パッケージの公式ドキュメント に方針が書かれています。 Operating systems provide both a “wall clock,” which is subject to changes for clock synchronization, and a “monotonic clock,” which is not. The general rule is that the wall clock is for telling time and the monotonic clock is for measuring time. Rather than split the API, in this package the Time returned by time.Now contains both a wall clock reading and a monotonic clock reading; later time-telling operations use the wall clock reading, but later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading. OSは「ウォールクロック」と「モノトニッククロック」の2つを提供している。ウォールクロックはクロック同期のために変更されうるが、モノトニッククロックは変更されない。一般的なルールとして、ウォールクロックは時刻を知るため(telling time)に、モノトニッククロックは時間を測るため(measuring time)に使う。本パッケージではAPIを分けるのではなく、 time.Now が返す Time にウォールクロックとモノトニッククロックの両方の読み取り値を含めることにしている。以降の「時刻を知る」操作はウォールクロックの値を、「時間を測る」操作(具体的には比較と差分)はモノトニッククロックの値を使う。 time.Timeの構造 「ウォール/モノトニックの両方を1つの Time で扱う」という方針を踏まえて、 src/time/time.go#L140-L161 で定義されている time.Time の構造を見てみましょう。 type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // unsigned wall seconds since Jan 1 year 1885, and ext holds a // signed 64-bit monotonic clock reading, nanoseconds since process start. wall uint64 ext int64 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location } 中身は wall 内に持っている hasMonotonic フラグによって2パターンに切り替わります。 hasMonotonic = 1 (ウォール+モノトニック) hasMonotonic = 0 (ウォールのみ) wall フラグ(1) + ウォール秒(33bit, 1885年起点) + ウォールナノ秒(30bit) フラグ(0) + 33bit秒は未使用(0) + ウォールナノ秒(30bit) ext モノトニッククロック値 (プロセス起動からのナノ秒) ウォール秒 (西暦1年起点の符号付き64bit) loc タイムゾーン タイムゾーン ext が hasMonotonic で意味を切り替えるようになっているのは、 wall の33bit秒(1885年起点)だと 1885〜2157年の約272年分 しか表現できないからです。 time.Date(1500, ...) のような範囲外の時刻を扱う hasMonotonic = 0 のケースでは、ウォール秒を wall の33bitから ext (int64, 西暦1年起点) に移してより広い範囲をカバーします。これは Time のサイズを増やさずに「モノトニック付き」と「広い時刻範囲」を両立させるための工夫です。次節の time.Now 実装の中にも、以下のように33bit上限に言及するコメントが出てきます。 // This will be true after March 16, 2157. time.Now 本体の実装 ここまで把握した上で、 src/time/time.go#L1347-L1361 にある time.Now の実装を見ていきます(Go 1.26.3 現在)。 // Now returns the current local time. func Now() Time { sec, nsec, mono := runtimeNow() if mono == 0 { return Time{ uint64 (nsec), sec + unixToInternal, Local} } mono -= startNano sec += unixToInternal - minWall if uint64 (sec)>> 33 != 0 { // Seconds field overflowed the 33 bits available when // storing a monotonic time. This will be true after // March 16, 2157. return Time{ uint64 (nsec), sec + minWall, Local} } return Time{hasMonotonic | uint64 (sec)<<nsecShift | uint64 (nsec), mono, Local} } コードに出てくる定数は同じく src/time/time.go の L163-L169 ( hasMonotonic など)と L535-L568 ( unixToInternal など)、および L1341 ( startNano )に以下のように定義されています。(簡単のため一部省略して記述しています) const ( secondsPerDay = 24 * 60 * 60 // 西暦1年1月1日 〜 UNIXエポック(1970-01-01) の秒数 unixToInternal int64 = ( 1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400 ) * secondsPerDay // 西暦1年1月1日 〜 1885年1月1日 の秒数 wallToInternal int64 = ( 1884 * 365 + 1884 / 4 - 1884 / 100 + 1884 / 400 ) * secondsPerDay ) const ( hasMonotonic = 1 << 63 // wall の最上位bitに立てるフラグ minWall = wallToInternal // wall の33bit秒の起点(= 1885年) nsecShift = 30 // wall に秒を詰めるときのシフト量 ) // プロセス起動時点のモノトニック値(runtime 初期化時にセットされる) var startNano int64 (1969*365 + 1969/4 - 1969/100 + 1969/400) の式は閏年を考慮してその年までの日数を計算しています。これに secondsPerDay を掛けることで「西暦1年1月1日からその年までの 秒数 」になります。 unixToInternal は1970年、 wallToInternal は1885年までのそれにあたります。 役割を整理すると以下のとおりです。 定数 役割 unixToInternal OSが返すUNIX秒を 「西暦1年起点」 に変換するオフセット minWall (= wallToInternal ) 西暦1年起点を 「1885年起点」 にずらすオフセット( wall の33bit秒の基準合わせ) nsecShift wall に秒を詰めるとき左に30bitシフトしてナノ秒の場所を空ける hasMonotonic モノトニックが入っているかのフラグ( wall の最上位bit) startNano プロセス起動時点のモノトニック値。これを引くことで ext を 「プロセス起動からの経過ns」 に正規化する Local loc に入れるデフォルトのタイムゾーン( *Location ) これらを踏まえてもう一度 time.Now を読み直すと、3パターンで Time を組み立てていることがわかります。 func Now() Time { // OSから現在のウォール秒・ナノ秒・モノトニック値を取得 sec, nsec, mono := runtimeNow() // 【パターン1】モノトニッククロックが取れなかった環境 // → ウォールクロックだけを ext に入れて返す(hasMonotonic = 0 のレイアウト) if mono == 0 { // sec(UNIX秒) + unixToInternal で「西暦1年起点の秒」に変換し、ext に詰める return Time{ uint64 (nsec), sec + unixToInternal, Local} } // 以降は mono あり mono -= startNano // モノトニックを「プロセス起動からの経過ns」に正規化 sec += unixToInternal - minWall // sec を「1885年起点の秒」に変換(33bit領域に詰める準備) // 【パターン2】33bitに収まらない(= 2157年3月16日以降) // → モノトニックを諦めて、ウォール秒は ext の方に置く(hasMonotonic = 0 のレイアウト) if uint64 (sec)>> 33 != 0 { // sec はいま1885年起点。minWall を足し戻して「西暦1年起点」に戻してから ext へ return Time{ uint64 (nsec), sec + minWall, Local} } // 【パターン3】通常パス // → hasMonotonic フラグを立て、ウォール・モノトニック両方を wall / ext に詰める // - sec は1885年起点のまま wall の33bit領域へ(<< nsecShift でナノ秒の場所を空けて | で合成) // - mono は正規化済みの値をそのまま ext へ return Time{hasMonotonic | uint64 (sec)<<nsecShift | uint64 (nsec), mono, Local} } runtimeNow() の中身 runtimeNow() は time パッケージ側ではシグネチャだけ書かれており、実体は src/runtime/time.go#L16-L31 にあります。 //go:linkname time_runtimeNow time.runtimeNow func time_runtimeNow() (sec int64 , nsec int32 , mono int64 ) { if bubble := getg().bubble; bubble != nil { sec = bubble.now / ( 1000 * 1000 * 1000 ) nsec = int32 (bubble.now % ( 1000 * 1000 * 1000 )) // Don't return a monotonic time inside a synctest bubble. // If we return a monotonic time based on the fake clock, // arithmetic on times created inside/outside bubbles is confusing. // If we return a monotonic time based on the real monotonic clock, // arithmetic on times created in the same bubble is confusing. // Simplest is to omit the monotonic time within a bubble. return sec, nsec, 0 } return time_now() } 分岐は2つあります。 synctest bubble の分岐 : 実行中のゴルーチンが synctest のバブル内にいる場合、バブルの仮想時刻 bubble.now を ウォール秒・ナノ秒として返し 、モノトニックは 0 を返します。 通常の分岐 : time_now() を呼び出します。これの実体はプラットフォーム別に実装されており、最終的にはどれもOSが提供する時刻取得APIを叩いています。 time_now() の実装は低レイヤーに近い話になるので今回は触れません。 bubble.now は、 src/runtime/synctest.go#L186-L187 の synctestRun で初期化されます。 const synctestBaseTime = 946684800000000000 // midnight UTC 2000-01-01 bubble.now = synctestBaseTime モノトニックを使わない理由については、コメントに書かれています。以下、和訳です。 synctestバブル内ではモノトニック時刻を返さないようにする。 フェイククロックに基づいたモノトニック時刻を返してしまうと、バブル内で作った時刻とバブル外で作った時刻のあいだでの計算結果が紛らわしくなる。 一方で実モノトニッククロックに基づいたモノトニック時刻を返しても、同じバブル内で作った時刻同士の計算が紛らわしくなる。 もっともシンプルな解は、バブル内ではモノトニック時刻を省略することだ。 モノトニッククロックはマシン起動を起点とするものなので、ウォールクロックだけ仮想時刻を進めても両者の値が食い違ってしまいます。一方で synctestBaseTime を起点にしたモノトニック値を別途用意するという選択肢もありますが、その場合もバブル外で取得した Time との差分計算で実時間とバブル内仮想時間が混在してしまいます。これらを避けるために、バブル内ではウォールクロックのみを扱う実装になっている、ということですね。 まとめ time.Now() の経路は以下のようになっていることがわかりました。 time.Now() │ │ ① (sec, nsec, mono) を取得 └── time.runtimeNow() ── linkname ──→ runtime.time_runtimeNow() │ ├── synctest bubble 内 │ sec = bubble.now / 1e9 (バブル仮想時刻) │ nsec = bubble.now % 1e9 │ mono = 0 (バブル内ではmonoを返さない) │ └── 通常経路 runtime.time_now() (プラットフォーム別実装) └─ OSが提供する時刻取得APIを呼ぶ │ │ ② 受け取った値を Time{wall, ext, loc} に組み立てて返す │ ├── mono == 0 → hasMonotonic=0, ext に「西暦1年起点ウォール秒」 ├── 通常 (33bit以内) → hasMonotonic=1, wall=フラグ|33bit秒(1885年起点)|30bitナノ秒, ext=mono(プロセス起動からのns) └── 33bit溢れ (2157年以降) → hasMonotonic=0, ext に「西暦1年起点ウォール秒」 Goが扱う時刻の構造とその設計理由、synctestの分岐実装、そしてウォールとモノトニックを巧みに組み合わせる工夫を知ることができ、とても満足しています。プラットフォームごとの実装は、まずは自分が使っているarm64から見てみたいと思います。 最後までお読みいただきありがとうございました! 参考 Package time - pkg.go.dev Package testing/synctest - pkg.go.dev Go: src/time/time.go Go: src/runtime/time.go Go: src/runtime/synctest.go Go 1.9 Release Notes - Monotonic Clocks Proposal: Monotonic Elapsed Time Measurements in Go