[インデックス 14586] ファイルの概要
このコミットは、Go言語のtime
パッケージにRound
およびTruncate
メソッドを追加するものです。これは、Go 1.1でLinux上でのtime.Now()
の精度がナノ秒に向上することに伴い、既存のマイクロ秒精度で時間を外部ストレージに保存しているコードが、読み戻した際に正確な時刻を復元できなくなる問題を解決するために導入されました。これらの新しいメソッドは、時間を外部に保存する前に、意図的に精度を落とす(丸める、切り捨てる)ことを可能にします。
コミット
commit 00cd6a3be31cd685c15fff1d7b8b62b286cc95f6
Author: Russ Cox <rsc@golang.org>
Date: Sun Dec 9 03:59:33 2012 -0500
time: add Round and Truncate
New in Go 1 will be nanosecond precision in the result of time.Now on Linux.
This will break code that stores time in external formats at microsecond
precision, reads it back, and expects to get exactly the same time.
Code like that can be fixed by using time.Now().Round(time.Microsecond)
instead of time.Now() in those contexts.
R=golang-dev, bradfitz, iant, remyoudompheng
CC=golang-dev
https://golang.org/cl/6903050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/00cd6a3be31cd685c15fff1d7b8b62b286cc95f6
元コミット内容
time: add Round and Truncate
New in Go 1 will be nanosecond precision in the result of time.Now on Linux.
This will break code that stores time in external formats at microsecond
precision, reads it back, and expects to get exactly the same time.
Code like that can be fixed by using time.Now().Round(time.Microsecond)
instead of time.Now() in those contexts.
R=golang-dev, bradfitz, iant, remyoudompheng
CC=golang-dev
https://golang.org/cl/6903050
変更の背景
この変更の主な背景は、Go 1.1リリースにおけるtime
パッケージの挙動変更にあります。具体的には、Linux環境においてtime.Now()
が返す時刻の精度が、従来のマイクロ秒(μs)からナノ秒(ns)へと向上することが決定されました。
この精度向上自体は一般的には望ましい進歩ですが、既存のアプリケーションにとっては互換性の問題を引き起こす可能性がありました。多くのシステムや外部ストレージ形式では、時刻情報をマイクロ秒精度で扱っていました。例えば、データベースのタイムスタンプカラムや、特定のプロトコルでやり取りされる時刻データなどがこれに該当します。
Go 1.1でナノ秒精度で取得された時刻を、マイクロ秒精度しか持たない外部形式に保存し、その後そのデータを読み戻した場合、ナノ秒以下の情報が失われるため、元の正確な時刻を復元できなくなります。これにより、特に時刻の比較や同一性チェックを行うロジックにおいて、予期せぬバグや不整合が発生する可能性がありました。
この問題を緩和するため、開発者は外部システムとの連携時に、Goのtime.Time
オブジェクトの精度を意図的に調整する必要が生じました。Round
とTruncate
メソッドは、このようなシナリオにおいて、開発者が時刻の精度を制御し、外部システムとの互換性を維持するための手段を提供します。例えば、time.Now().Round(time.Microsecond)
を使用することで、ナノ秒精度で取得された時刻をマイクロ秒精度に丸めてから外部に保存することが可能になります。
前提知識の解説
Go言語のtime
パッケージ
Go言語の標準ライブラリであるtime
パッケージは、時刻の表現、操作、表示に関する機能を提供します。
time.Time
: 特定の時点を表す構造体です。内部的には、Unixエポック(1970年1月1日UTC)からの経過秒数とナノ秒数を保持しています。time.Duration
: 期間、つまり時間の長さを表す型です。ナノ秒単位で表現され、time.Second
、time.Minute
、time.Hour
などの定数を使って期間を指定できます。例えば、time.Microsecond
は1マイクロ秒の期間を表します。
時刻の精度
時刻の精度とは、時刻をどれだけ細かく表現できるかを示します。
- マイクロ秒精度: 100万分の1秒(10^-6秒)単位で時刻を表現します。
- ナノ秒精度: 10億分の1秒(10^-9秒)単位で時刻を表現します。ナノ秒はマイクロ秒よりも1000倍細かい精度です。
丸め(Rounding)と切り捨て(Truncating)
数値処理における丸めと切り捨ては、精度を調整する一般的な操作です。
- 丸め(Round): 指定された単位に最も近い値に調整します。例えば、12.345を小数点以下第2位で丸めると12.35になります(四捨五入の場合)。時刻の場合、指定された期間の最も近い倍数に時刻を調整します。通常、中間値(例:0.5)は切り上げられます。
- 切り捨て(Truncate): 指定された単位の倍数になるように、常に値を小さい方向(ゼロ方向)に調整します。例えば、12.345を小数点以下第2位で切り捨てると12.34になります。時刻の場合、指定された期間の倍数になるように、常に過去方向(ゼロ時刻方向)に時刻を調整します。
これらの操作は、異なる精度を持つシステム間でデータをやり取りする際に、データの整合性を保つために重要です。
技術的詳細
このコミットでは、time.Time
型にRound(d Duration)
とTruncate(d Duration)
という2つの新しいメソッドが追加されました。これらのメソッドは、time.Time
オブジェクトが保持する時刻を、指定されたDuration
の倍数に丸めたり、切り捨てたりする機能を提供します。
Time.Truncate(d Duration) Time
Truncate
メソッドは、t
をd
の倍数に切り捨てた結果を返します。これは、t
からt
をd
で割った余り(r
)を引くことで実現されます。
d <= 0
の場合、t
は変更されずに返されます。- 内部的には、
div(t, d)
関数を呼び出して、t
をd
で割った余りr
を計算します。 - その後、
t.Add(-r)
によって、t
から余りを引くことで切り捨てを行います。これにより、時刻は常にd
の最も近い過去の倍数に調整されます。
Time.Round(d Duration) Time
Round
メソッドは、t
をd
の最も近い倍数に丸めた結果を返します。中間値(ちょうど半分)は切り上げられます。
d <= 0
の場合、t
は変更されずに返されます。Truncate
と同様に、div(t, d)
関数を呼び出して余りr
を計算します。- 丸めのロジックは、
r
がd
の半分よりも大きいか、またはちょうど半分である場合に、t
にd - r
を加えることで次のd
の倍数に丸めます。それ以外の場合は、t
からr
を引くことで切り捨てと同じように丸めます。if r+r < d
: 余りr
がd
の半分よりも小さい場合、t.Add(-r)
で切り捨てます。else
: 余りr
がd
の半分以上の場合、t.Add(d - r)
で切り上げます。
div(t Time, d Duration) (qmod2 int, r Duration)
Round
とTruncate
の内部で利用されるヘルパー関数です。t
(time.Time
)をd
(time.Duration
)で割り、商のパリティ(qmod2
)と余り(r
)を返します。
この関数は、time.Time
が内部的に秒とナノ秒で表現されていることを考慮し、正確な除算と余りの計算を行います。特に、time.Time
が負の値を持つ場合や、d
が非常に小さい(ナノ秒単位)または非常に大きい(秒の倍数)場合に対応するための特殊なケース処理が含まれています。
- 負の時刻の処理:
t
が負の場合、絶対値で操作し、最後に結果を調整します。 - 特殊ケース:
d
が1秒未満で、かつ2d
が1秒を割り切る場合(例:d = 3ns
)。d
が1秒の倍数である場合。
- 一般ケース: 上記の特殊ケースに該当しない場合、
t
のナノ秒表現を128ビットの数値として扱い、ビットシフトと減算を繰り返すことで商と余りを計算します。これは、非常に大きな数値の除算を効率的に行うための手法です。
このdiv
関数は、time.Time
の内部表現とtime.Duration
の特性を深く理解している必要があり、正確な丸めと切り捨てを実現するための基盤となっています。
コアとなるコードの変更箇所
このコミットでは、以下の4つのファイルが変更されています。
doc/go1.1.html
: Go 1.1のリリースノートに、time
パッケージの変更(Linuxでのナノ秒精度化とRound
/Truncate
の追加)に関する説明が追記されました。time
パッケージのセクションが追加され、Linuxでのナノ秒精度への変更と、それによって生じる互換性問題、そしてRound
とTruncate
がその解決策として導入されたことが記述されています。
src/pkg/time/example_test.go
:time.Round
とtime.Truncate
の使用例を示すテストコードが追加されました。ExampleTime_Round()
関数とExampleTime_Truncate()
関数が追加され、様々なDuration
で時刻を丸めたり切り捨てたりした場合の出力例が示されています。これは、新しいAPIの動作を開発者に明確に伝えるための重要なドキュメントです。
src/pkg/time/time.go
:time.Time
型にRound
とTruncate
メソッド、およびそれらをサポートする内部ヘルパー関数div
が実装されました。Time.Truncate(d Duration) Time
メソッドの定義と実装。Time.Round(d Duration) Time
メソッドの定義と実装。div(t Time, d Duration) (qmod2 int, r Duration)
ヘルパー関数の定義と実装。この関数は、時刻の除算と余りを計算する複雑なロジックを含んでいます。
src/pkg/time/time_test.go
:time.Round
とtime.Truncate
の動作を検証するための包括的なテストが追加されました。abs
およびabsString
ヘルパー関数が追加され、テスト内で絶対時刻を文字列として表現するために使用されます。truncateRoundTests
という構造体のスライスが定義され、手動で定義されたテストケースが含まれています。TestTruncateRound
関数が追加され、様々なシナリオ(手動テストケース、0付近の網羅的テスト、ランダム生成テストケース、秒の約数、秒の倍数、中間値ケースなど)でRound
とTruncate
の正確性を検証しています。特に、math/big
パッケージを使用して、高精度な計算を行い、Goのtime
パッケージの計算結果と比較することで、正確性を保証しています。
コアとなるコードの解説
このコミットのコアとなるコードは、src/pkg/time/time.go
に追加されたTime.Truncate
、Time.Round
、そして内部ヘルパー関数div
です。
Time.Truncate
メソッド
// Truncate returns the result of rounding t down to a multiple of d (since the zero time).
// If d <= 0, Truncate returns t unchanged.
func (t Time) Truncate(d Duration) Time {
if d <= 0 {
return t
}
_, r := div(t, d) // tをdで割った余りrを計算
return t.Add(-r) // tから余りを引くことで切り捨て
}
このメソッドは、与えられたDuration d
の倍数になるように、Time t
を切り捨てます。例えば、t
が12:15:30.918273645
でd
がtime.Microsecond
の場合、結果は12:15:30.918273
となります。これは、t
をd
で割った余りをt
から引くことで実現されます。
Time.Round
メソッド
// Round returns the result of rounding t to the nearest multiple of d (since the zero time).
// The rounding behavior for halfway values is to round up.
// If d <= 0, Round returns t unchanged.
func (t Time) Round(d Duration) Time {
if d <= 0 {
return t
}
_, r := div(t, d) // tをdで割った余りrを計算
if r+r < d { // 余りrがdの半分より小さい場合
return t.Add(-r) // 切り捨て
}
return t.Add(d - r) // 切り上げ
}
このメソッドは、与えられたDuration d
の最も近い倍数になるように、Time t
を丸めます。中間値(ちょうど半分)は切り上げられます。例えば、t
が12:15:30.918273645
でd
がtime.Microsecond
の場合、結果は12:15:30.918274
となります。これは、余りr
がd
の半分未満であれば切り捨て、そうでなければ切り上げるというロジックで実装されています。
div
ヘルパー関数
// div divides t by d and returns the quotient parity and remainder.
// We don't use the quotient parity anymore (round half up instead of round to even)
// but it's still here in case we change our minds.
func div(t Time, d Duration) (qmod2 int, r Duration) {
neg := false
if t.sec < 0 {
// Operate on absolute value.
neg = true
t.sec = -t.sec
t.nsec = -t.nsec
if t.nsec < 0 {
t.nsec += 1e9
t.sec-- // t.sec >= 1 before the -- so safe
}
}
switch {
// Special case: 2d divides 1 second.
case d < Second && Second%(d+d) == 0:
qmod2 = int(t.nsec/int32(d)) & 1
r = Duration(t.nsec % int32(d))
// Special case: d is a multiple of 1 second.
case d%Second == 0:
d1 := int64(d / Second)
qmod2 = int(t.sec/d1) & 1
r = Duration(t.sec%d1)*Second + Duration(t.nsec)
// General case.
// This could be faster if more cleverness were applied,
// but it's really only here to avoid special case restrictions in the API.
// No one will care about these cases.
default:
// Compute nanoseconds as 128-bit number.
sec := uint64(t.sec)
tmp := (sec >> 32) * 1e9
u1 := tmp >> 32
u0 := tmp << 32
tmp = uint64(sec&0xFFFFFFFF) * 1e9
u0x, u0 := u0, u0+tmp
if u0 < u0x {
u1++
}
u0x, u0 = u0, u0+uint64(t.nsec)
if u0 < u0x {
u1++
}
// Compute remainder by subtracting r<<k for decreasing k.
// Quotient parity is whether we subtract on last round.
d1 := uint64(d)
for d1>>63 != 1 {
d1 <<= 1
}
d0 := uint64(0)
for {
qmod2 = 0
if u1 > d1 || u1 == d1 && u0 >= d0 {
// subtract
qmod2 = 1
u0x, u0 = u0, u0-d0
if u0 > u0x {
u1--
}
u1 -= d1
}
if d1 == 0 && d0 == uint64(d) {
break
}
d0 >>= 1
d0 |= (d1 & 1) << 63
d1 >>= 1
}
r = Duration(u0)
}
if neg && r != 0 {
// If input was negative and not an exact multiple of d, we computed q, r such that
// q*d + r = -t
// But the right answers are given by -(q-1), d-r:
// q*d + r = -t
// -q*d - r = t
// -(q-1)*d + (d - r) = t
qmod2 ^= 1
r = d - r
}
return
}
div
関数は、Time
とDuration
の除算を行い、余りを返します。これは、Time
が内部的に秒とナノ秒で表現されているため、単純な数値除算では対応できない複雑なケースを処理するために必要です。特に、負の時刻の扱い、1秒未満のDurationや1秒の倍数のDurationに対する最適化、そして一般的なケースでの128ビット演算による高精度な余りの計算が含まれています。この関数の複雑さは、Goのtime
パッケージが提供する時刻操作の正確性と堅牢性を保証するために不可欠です。
関連リンク
- Go言語の
time
パッケージ公式ドキュメント - Go 1.1 Release Notes - time package (このコミットで追加された
doc/go1.1.html
の内容が反映されています)
参考にした情報源リンク
- https://github.com/golang/go/commit/00cd6a3be31cd685c15fff1d7b8b62b286cc95f6
- Go言語の
time
パッケージ公式ドキュメント - Go 1.1 Release Notes
- Go CL 6903050 (元のコードレビューリクエスト)
- Go time package source code (特に
time.go
とtime_test.go
)