⏱️

time.Durationで期間を扱うときに気をつけること

2024/12/23に公開

これはUnipos Advent Calendar 2024の記事です。

time.Duration の扱いに関する、ちょっとしたハマりポイントについて説明します。

データの有効期限を動的に設定できるようにしたうえで、期限切れ判定をしたい

期間判定をするつもりの、こういうコードがありました。

var ValidityPeriodDays = 30  // 実際はDBとかから取得する
createdAt := time.Date(2024, 12, 5, 0, 0, 0, 0, time.UTC)  // 仮
year, month, date := createdAt.Add(time.Hour * 24 * time.Duration(ValidityPeriodDays)).Date()
validityPeriod := time.Date(year, month, date, 0, 0, 0, 0, time.UTC)

fmt.Println(time.Now().After(validityPeriod))  // 期限切れてあればtrue
// 実際はこの判定結果でなにかする

https://go.dev/play/p/OD1zqXG-RVv

この実装が起こした問題

実質無限にするつもりで ValidityPeriodDays200000 を代入したところ、判定が常に失敗するようになりました。

何がいけなかったか

time.Duration はナノ秒をint64で持つ実装になっているので、符号つき64bit整数で表現できるナノ秒の範囲でしか時間を扱えません。
https://github.com/golang/go/issues/32501#issuecomment-500181393
https://pkg.go.dev/time#Duration

time.Duration が扱える期間を日数に直すとすると、106751日までとなります(気になる方はPlaygroundのコードで試してみてください)。

9223372036854775807/(1000 * 1000 * 1000 * 60 * 60 * 24) \approx 106751.99

対応方法

どんな値でも正しく計算するようにしたいのであれば、日数に相当する time.Duration を加算するのではなく、time.Time 型の AddDate メソッドを使って日数を加算すればよいです[1]

year, month, date := createdAt.AddDate(0, 0, ValidityPeriodDays).Date()

https://go.dev/play/p/EkDsZ8elQd6

ただ、現実的にはたとえば50万日後に期限を設定したいということはほとんどないと思うので、time.Duration 型が扱える範囲で ValiditiyPeriodDays に入る値のバリデーションをしたうえで、実装はそのまま、でもいいと思います。

まとめ

  • デカい期間を扱うときは time.Duration を使っていいか考えよう
  • 制約はだいたいドキュメントに書いてあるからちゃんと読もう
脚注
  1. AddDateメソッドは日数を渡すだけでいい感じの日付表現に正規化してくれる(1月30日に5日足して1月35日、とかにはならず2月4日と解釈される)ので、とにかくn日後を計算してほしい!というパターンではうれしい ↩︎

Discussion