[インデックス 11470] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージにおいて、NaN
(Not a Number) をマップのキーとして使用した場合のテストケースを追加するものです。具体的には、fmt_test.go
ファイルに、float64
型のNaN
をキーに持つマップのフォーマットに関するテストが追加されました。
コミット
commit e451fb8ffbca501b12611f97ec875e9544339aa0
Author: Russ Cox <rsc@golang.org>
Date: Mon Jan 30 13:20:38 2012 -0500
fmt: add test of NaN map keys
R=r
CC=golang-dev
https://golang.org/cl/5564063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e451fb8ffbca501b12611f97ec875e9544339aa0
元コミット内容
fmt: add test of NaN map keys
R=r
CC=golang-dev
https://golang.org/cl/5564063
変更の背景
このコミットの背景には、Go言語のfmt
パッケージがマップをフォーマットする際の特定の挙動、特にNaN
(Not a Number)がマップのキーとして使用された場合の挙動を明確にし、テストで保証するという目的があります。
浮動小数点数におけるNaN
は、数値ではない状態(例: 0/0、無限大 - 無限大)を表す特殊な値です。IEEE 754浮動小数点標準では、NaN
はそれ自身と等しくないという特性(NaN == NaN
がfalse
になる)を持ちます。この特性は、マップ(ハッシュテーブル)のキーとして使用される場合に特有の課題を引き起こします。
Goのマップはキーの等価性に基づいて動作します。通常、キーの比較には==
演算子が使用されます。しかし、NaN == NaN
がfalse
であるため、異なるNaN
値(ビットパターンが異なる場合がある)や、同じNaN
値であっても、マップがキーを検索する際に問題が生じる可能性があります。
このコミットが作成された2012年当時、Goのfmt
パッケージがマップを文字列として出力する際に、NaN
をキーに持つマップがどのように表示されるかについて、明確なテストケースが存在しなかった可能性があります。このテストの追加は、このようなエッジケースにおけるfmt
パッケージの出力が期待通りであることを保証し、将来的な回帰を防ぐためのものです。
特に、コメントで言及されているように、マップの出力は「まずキーのリストを取得し、次に各キーをルックアップする」というプロセスで行われます。NaN
はマップのキーにはなり得ますが、その特殊な等価性のため、直接ルックアップしようとすると失敗し、結果としてゼロ値のreflect.Value
が返され、それが<nil>
としてフォーマットされるという挙動が確認されています。この挙動を明示的にテストすることで、開発者はこの特定の出力形式を認識し、依存できるようになります。
前提知識の解説
1. Go言語のfmt
パッケージ
fmt
パッケージは、Go言語におけるフォーマットI/Oを実装するためのパッケージです。C言語のprintf
やscanf
に似た機能を提供し、様々なデータ型を文字列に変換して出力したり、文字列からデータを読み込んだりすることができます。
fmt.Sprintf
: フォーマットされた文字列を返します。%v
バーブ: 値のデフォルトフォーマットで出力します。構造体、マップ、スライスなど、あらゆる型の値を表示する際に便利です。マップの場合、キーと値のペアがmap[key:value key:value]
のような形式で出力されます。
2. 浮動小数点数とNaN
(Not a Number)
- 浮動小数点数: コンピュータで実数を近似的に表現するための形式です。Go言語では
float32
とfloat64
があります。 - IEEE 754標準: 浮動小数点数の表現と演算に関する国際標準です。
NaN
、+Inf
(正の無限大)、-Inf
(負の無限大)などの特殊な値も定義しています。 NaN
(Not a Number): 数値ではない結果を表す特殊な浮動小数点値です。例えば、0.0 / 0.0
やmath.Sqrt(-1.0)
のような演算結果として生成されます。NaN
の特性: IEEE 754標準において、NaN
はそれ自身を含め、いかなる値とも等しくありません。つまり、NaN == NaN
は常にfalse
になります。これは、通常の数値の比較とは異なる非常に重要な特性です。Go言語ではmath.NaN()
関数でNaN
値を取得できます。
3. Go言語のマップ (Map)
- Go言語のマップは、キーと値のペアを格納するハッシュテーブルの実装です。
- キーの要件: マップのキーとして使用できる型は、等価性(
==
演算子で比較可能であること)が定義されている型に限られます。これは、マップがキーのハッシュ値を計算し、そのハッシュ値とキーの等価性比較によって要素を効率的に検索・挿入・削除するためです。 NaN
とマップのキー:float64
型は等価性が定義されているため、マップのキーとして使用できます。しかし、NaN == NaN
がfalse
であるというNaN
の特殊な性質が、マップの動作に影響を与えます。Goのマップは、異なるNaN
値(たとえビットパターンが同じでも)を異なるキーとして扱うことができます。これは、NaN
が等価ではないため、マップがキーを区別する際に、通常の等価性比較では同じNaN
を「同じキー」として認識できないためです。
4. reflect
パッケージとreflect.Value
reflect
パッケージ: Go言語のreflect
パッケージは、実行時にプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、変数の型、値、メソッドなどを動的に調べたり、操作したりすることができます。reflect.Value
:reflect
パッケージの中心的な型の一つで、Goの任意の値を抽象的に表現します。reflect.Value
は、その値の型情報や、その値自体へのアクセスを提供します。- ゼロ値: Goの各型にはゼロ値があります。例えば、数値型は
0
、文字列型は""
、ポインタ型はnil
です。reflect.Value
のゼロ値は、有効な値を表さない状態です。
技術的詳細
このコミットで追加されたテストケースは、fmt
パッケージがmap[float64]int
型のマップをフォーマットする際の、NaN
キーの特殊な挙動を検証しています。
// The "<nil>" show up because maps are printed by
// first obtaining a list of keys and then looking up
// each key. Since NaNs can be map keys but cannot
// be fetched directly, the lookup fails and returns a
// zero reflect.Value, which formats as <nil>.
// This test is just to check that it shows the two NaNs at all.
{"%v", map[float64]int{math.NaN(): 1, math.NaN(): 2}, "map[NaN:<nil> NaN:<nil>]"},
このコメントとテストケースから、以下の技術的詳細が読み取れます。
-
NaN
はマップのキーになり得る:map[float64]int{math.NaN(): 1, math.NaN(): 2}
という記述から、float64
型のNaN
がGoのマップのキーとして有効であることが確認できます。Goのマップは、NaN
の特殊な等価性(NaN == NaN
がfalse
)のため、異なるNaN
値を異なるキーとして扱います。したがって、math.NaN(): 1
とmath.NaN(): 2
は、たとえ同じmath.NaN()
関数から生成されたものであっても、マップ内では異なるキーとして認識され、両方がマップに格納されます。 -
fmt
パッケージのマップ出力メカニズム:fmt
パッケージがマップをフォーマットする際、内部的には以下の手順を踏みます。- まず、マップからすべてのキーのリストを取得します。
- 次に、取得した各キーに対して、マップから対応する値をルックアップします。
-
NaN
キーのルックアップの失敗とreflect.Value
のゼロ値: ここがこのテストの核心です。NaN
はマップのキーとして存在しますが、その特殊な等価性のため、fmt
パッケージがキーのリストを取得した後、そのキーを使って再度マップから値を「直接フェッチ」しようとすると、内部的なルックアップメカニズムがNaN
の等価性ルールに阻まれ、期待通りに値を取得できない場合があります。- この「ルックアップの失敗」の結果、
reflect
パッケージを通じて値を取得しようとした際に、有効なreflect.Value
ではなく、その型のゼロ値(この場合はreflect.Value
のゼロ値)が返されます。 reflect.Value
のゼロ値は、fmt
パッケージによって文字列にフォーマットされると、<nil>
として表示されます。これは、ポインタやインターフェースのゼロ値がnil
として表示されるのと同様の挙動です。
-
テストの目的: コメントにあるように、このテストの目的は「2つの
NaN
が少なくとも表示されることを確認する」ことです。つまり、NaN
キーが存在し、それらがマップの出力に含まれること、そしてその値が<nil>
として表示されるという、この特殊な挙動が意図されたものであることを保証しています。これは、NaN
キーを持つマップのフォーマットが、ユーザーにとって予期せぬ結果とならないようにするための重要なテストです。
この挙動は、NaN
の等価性に関するIEEE 754の厳密なルールと、Goのマップおよびリフレクションの内部実装が組み合わさった結果として生じます。fmt
パッケージは、このような複雑なケースでも一貫した出力を提供しようと努めており、このテストはその一貫性を保証するものです。
コアとなるコードの変更箇所
変更はsrc/pkg/fmt/fmt_test.go
ファイルに集中しています。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -443,6 +443,14 @@ var fmttests = []struct {
{"%s", nil, "%!s(<nil>)"},
{"%T", nil, "<nil>"},
{"%-1", 100, "%!(NOVERB)%!(EXTRA int=100)"},
++
++ // The "<nil>" show up because maps are printed by
++ // first obtaining a list of keys and then looking up
++ // each key. Since NaNs can be map keys but cannot
++ // be fetched directly, the lookup fails and returns a
++ // zero reflect.Value, which formats as <nil>.
++ // This test is just to check that it shows the two NaNs at all.
++ {"%v", map[float64]int{math.NaN(): 1, math.NaN(): 2}, "map[NaN:<nil> NaN:<nil>]"},
}
func TestSprintf(t *testing.T) {
コアとなるコードの解説
追加されたコードは、fmttests
というグローバル変数(struct
のスライス)に新しいテストケースを追加しています。このスライスは、fmt
パッケージの様々なフォーマット動作をテストするために使用されます。
新しいテストケースの構造は以下の通りです。
{"%v", map[float64]int{math.NaN(): 1, math.NaN(): 2}, "map[NaN:<nil> NaN:<nil>]"},
"%v"
: これはフォーマット文字列です。%v
は、Goの値のデフォルト表現を出力するために使用されるバーブです。マップの場合、キーと値のペアがmap[key:value]
の形式で出力されます。map[float64]int{math.NaN(): 1, math.NaN(): 2}
: これはテスト対象となる入力値です。float64
をキー、int
を値とするマップを定義しています。注目すべきは、キーとしてmath.NaN()
が2回使用されている点です。前述の通り、NaN == NaN
がfalse
であるため、Goのマップはこれら2つのNaN
を異なるキーとして扱います。したがって、このマップには2つのエントリが含まれます。"map[NaN:<nil> NaN:<nil>]"
: これは、上記のマップを%v
でフォーマットした際に期待される出力文字列です。map[...]
はマップの標準的な出力形式です。NaN
はキーとしてそのまま表示されます。<nil>
は、対応する値が表示されるべき場所です。コメントで説明されているように、fmt
パッケージがマップのキーを列挙し、その後各キーに対応する値をルックアップしようとすると、NaN
の特殊な等価性のためルックアップが失敗し、結果としてreflect.Value
のゼロ値が返されます。このゼロ値がfmt
によって<nil>
としてフォーマットされるため、このような出力になります。
このテストケースは、fmt
パッケージがNaN
をキーに持つマップをどのように処理し、どのような出力を生成するかという、特定の(そしてやや複雑な)挙動を明確に定義し、検証しています。これにより、この挙動が将来の変更によって意図せず変更されることを防ぎ、開発者がこの出力に依存できるようになります。
関連リンク
- Go言語の
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語の
math
パッケージのドキュメント(NaN
について): https://pkg.go.dev/math#NaN - Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - IEEE 754 浮動小数点標準 (Wikipedia): https://ja.wikipedia.org/wiki/IEEE_754
参考にした情報源リンク
- Go言語の公式ドキュメント (
fmt
,math
,reflect
パッケージ) - IEEE 754浮動小数点標準に関する一般的な情報源
- Go言語のマップの動作に関する一般的な情報源
- Go言語のコミット履歴と関連するコードレビュー (Go CL 5564063)