Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14144] ファイルの概要

このコミットは、Go言語の標準ライブラリtimeパッケージにおけるParseDuration関数の挙動を修正するものです。具体的には、期間を表す文字列(例: "52763797000ns")に含まれる数値が32ビット符号付き整数の最大値(約2 * 10^9)を超える場合に、正しくパースできるように改善されました。

コミット

commit f2045aadd9bd09b1d1371ee9bc2817f50d7587fd
Author: David Symonds <dsymonds@golang.org>
Date:   Mon Oct 15 07:50:13 2012 +1100

    time: accept numbers larger than 2^32 in ParseDuration.
    
    Fixes #3374.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6683047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f2045aadd9bd09b1d1371ee9bc2817f50d7587fd

元コミット内容

time: accept numbers larger than 2^32 in ParseDuration.

Fixes #3374.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6683047

変更の背景

Go言語のtimeパッケージには、期間を表す文字列(例: "1h30m", "5s")をtime.Duration型に変換するParseDuration関数があります。この関数は、内部的に文字列から数値を抽出するためにleadingIntというヘルパー関数を使用しています。

以前の実装では、leadingInt関数が数値をint型(多くのシステムで32ビット符号付き整数)として扱っていました。このため、期間文字列に含まれる数値がint型の最大値(2^31 - 1、約21億)を超えると、オーバーフローが発生し、ParseDurationが正しく動作しない、または予期せぬ結果を返すという問題がありました。

具体的には、Go issue #3374として報告されており、例えば非常に長い期間(例: 52763797000ナノ秒)を表現しようとすると、この制限に引っかかっていました。このような大きな数値は、特にナノ秒単位で時間を扱う場合や、非常に長いタイムアウト期間などを設定する際に必要となることがあります。このコミットは、このint型の制限を緩和し、より大きな数値を扱えるようにすることで、ParseDuration関数の堅牢性と実用性を向上させることを目的としています。

前提知識の解説

  • Go言語のtimeパッケージ: Go言語の標準ライブラリの一部で、時間、期間、日付などを扱うための機能を提供します。time.Duration型は、ナノ秒単位で時間を表す整数型です。
  • time.ParseDuration関数: timeパッケージの関数で、"1h30m"のような期間文字列をtime.Duration型にパースします。
  • int型とint64: Go言語における整数型です。
    • int: 実行環境に依存する符号付き整数型で、通常は32ビットまたは64ビットです。多くのシステムでは32ビットとして実装されています。32ビットの場合、表現できる値の範囲は約 -2 * 10^9 から 2 * 10^9 です。
    • int64: 常に64ビットの符号付き整数型です。表現できる値の範囲は非常に大きく、約 -9 * 10^18 から 9 * 10^18 です。
  • オーバーフロー: 変数に格納できる最大値を超えた数値を代入しようとしたときに発生する現象です。これにより、値が予期せぬものになったり、エラーが発生したりします。
  • leadingInt関数: このコミットで変更される内部ヘルパー関数で、文字列の先頭から連続する数字を整数として抽出し、残りの文字列を返す役割を担っています。

技術的詳細

このコミットの主要な技術的変更点は、timeパッケージ内の数値パース処理において、32ビット整数型(int)の代わりに64ビット整数型(int64)を使用するように変更したことです。

具体的には、以下の関数と関連するロジックが修正されました。

  1. leadingInt関数の戻り値の型変更:
    • 以前はint型を返していましたが、int64型を返すように変更されました。これにより、この関数がパースできる数値の最大値が、32ビット整数の約2 * 10^9から64ビット整数の約9 * 10^18へと大幅に拡張されました。
  2. leadingInt関数内のオーバーフローチェックの更新:
    • 数値のパース中にオーバーフローが発生しないようにするためのチェックロジックが、int型の最大値(1<<31-10)からint64型の最大値(1<<63-10)に対応するように更新されました。これにより、より大きな数値でも安全に処理できるようになりました。
  3. atoi関数とParseDuration関数でのleadingIntの利用方法の調整:
    • atoi関数(文字列を整数に変換する内部関数)とParseDuration関数は、leadingIntの戻り値がint64になったことに合わせて、受け取る変数の型をintからint64に変更しました。atoi関数では、最終的にint型にキャストし直していますが、これはatoiが返す値が通常はintの範囲に収まることを想定しているためです。しかし、leadingIntがより大きな値を処理できるようになったことで、ParseDurationが間接的に大きな数値を扱えるようになりました。

これらの変更により、ParseDurationは、例えば"52763797000ns"のような、32ビット整数では表現できない大きなナノ秒単位の期間も正確にパースできるようになりました。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルと関数は以下の通りです。

  • src/pkg/time/format.go:
    • atoi関数: leadingIntの戻り値を受け取る変数の型がintからint64に変更され、その後intにキャストされるようになりました。
    • leadingInt関数:
      • 関数のシグネチャがfunc leadingInt(s string) (x int, rem string, err error)からfunc leadingInt(s string) (x int64, rem string, err error)に変更されました。
      • オーバーフローチェックの条件がx >= (1<<31-10)/10からx >= (1<<63-10)/10に変更されました。
      • 数値の加算ロジックがx = x*10 + int(c) - '0'からx = x*10 + int64(c) - '0'に変更されました。
    • ParseDuration関数: leadingIntの戻り値を受け取る変数の型がvar x intからvar x int64に変更されました。
  • src/pkg/time/time_test.go:
    • parseDurationTests変数に、大きな数値を含む新しいテストケース{"52763797000ns", true, 52763797000 * Nanosecond}が追加されました。

コアとなるコードの解説

以下に、変更された主要なコードスニペットとその解説を示します。

src/pkg/time/format.goleadingInt 関数

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -954,18 +955,18 @@ func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string,
 var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
 
 // leadingInt consumes the leading [0-9]* from s.
-func leadingInt(s string) (x int, rem string, err error) {
+func leadingInt(s string) (x int64, rem string, err error) {
  	i := 0
  	for ; i < len(s); i++ {
  	\tc := s[i]
  	\tif c < '0' || c > '9' {
  	\t\tbreak
  	\t}
-\t\tif x >= (1<<31-10)/10 {
+\t\tif x >= (1<<63-10)/10 {
  	\t\t// overflow
  	\t\treturn 0, "", errLeadingInt
  	\t}
-\t\tx = x*10 + int(c) - '0'
+\t\tx = x*10 + int64(c) - '0'
  	}
  	return x, s[i:], nil
  }
  • func leadingInt(s string) (x int64, rem string, err error):
    • この行は、leadingInt関数の戻り値の型がintからint64に変更されたことを示しています。これにより、x変数に格納できる数値の範囲が拡張されます。
  • if x >= (1<<63-10)/10:
    • これはオーバーフローチェックの条件です。1<<63-1は64ビット符号付き整数の最大値です。(1<<63-10)/10は、次の桁を読み込んだときにオーバーフローしないようにするための安全マージンを考慮したチェックです。この変更により、int64の範囲でのオーバーフローを検出できるようになりました。
  • x = x*10 + int64(c) - '0':
    • 文字列から抽出した数字cint64にキャストしてからxに加算しています。これにより、計算中に中間的なオーバーフローが発生するのを防ぎ、xint64として正しく更新されることを保証します。

src/pkg/time/format.goatoi 関数

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -323,7 +323,8 @@ func atoi(s string) (x int, err error) {
  	\t\tneg = true
  	\t\ts = s[1:]
  	\t}
-\t\tx, rem, err := leadingInt(s)
+\t\tq, rem, err := leadingInt(s)
+\t\tx = int(q)
  	\tif err != nil || rem != "" {
  	\t\treturn 0, atoiError
  	\t}
  • q, rem, err := leadingInt(s):
    • leadingIntint64を返すようになったため、その戻り値を受け取る新しい変数qint64型として宣言されます。
  • x = int(q):
    • atoi関数自体の戻り値はint型であるため、leadingIntから得られたint64qint型に明示的にキャストしています。これは、atoiが通常パースする数値がintの範囲に収まることを前提としています。

src/pkg/time/format.goParseDuration 関数

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -1010,7 +1011,7 @@ func ParseDuration(s string) (Duration, error) {
  	for s != "" {
  	\tg := float64(0) // this element of the sequence
  
-\t\tvar x int
+\t\tvar x int64
  	\tvar err error
  
  	\t// The next character must be [0-9.]
  • var x int64:
    • ParseDuration関数内でleadingIntの戻り値を受け取る変数xの型がintからint64に変更されました。これにより、ParseDurationleadingIntが返す大きな数値を直接扱うことができるようになります。

src/pkg/time/time_test.go のテストケース追加

--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -999,6 +999,8 @@ var parseDurationTests = []struct {
  	{\"-2m3.4s\", true, -(2*Minute + 3*Second + 400*Millisecond)},\n  	{\"1h2m3s4ms5us6ns\", true, 1*Hour + 2*Minute + 3*Second + 4*Millisecond + 5*Microsecond + 6*Nanosecond},\n  	{\"39h9m14.425s\", true, 39*Hour + 9*Minute + 14*Second + 425*Millisecond},\n+\t// large value\n+\t{\"52763797000ns\", true, 52763797000 * Nanosecond},\n  \n  	// errors\n  	{\"\", false, 0},\
  • {"52763797000ns", true, 52763797000 * Nanosecond}:
    • この新しいテストケースは、32ビット整数では表現できない大きな数値(527億ナノ秒)を含む期間文字列が正しくパースされることを検証します。これは、今回の変更が意図通りに機能していることを確認するための重要なテストです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: timeパッケージ
  • Go言語のソースコード: src/pkg/time/format.go, src/pkg/time/time_test.go
  • Go issue #3374の議論