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

[インデックス 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 == NaNfalseになる)を持ちます。この特性は、マップ(ハッシュテーブル)のキーとして使用される場合に特有の課題を引き起こします。

Goのマップはキーの等価性に基づいて動作します。通常、キーの比較には==演算子が使用されます。しかし、NaN == NaNfalseであるため、異なるNaN値(ビットパターンが異なる場合がある)や、同じNaN値であっても、マップがキーを検索する際に問題が生じる可能性があります。

このコミットが作成された2012年当時、Goのfmtパッケージがマップを文字列として出力する際に、NaNをキーに持つマップがどのように表示されるかについて、明確なテストケースが存在しなかった可能性があります。このテストの追加は、このようなエッジケースにおけるfmtパッケージの出力が期待通りであることを保証し、将来的な回帰を防ぐためのものです。

特に、コメントで言及されているように、マップの出力は「まずキーのリストを取得し、次に各キーをルックアップする」というプロセスで行われます。NaNはマップのキーにはなり得ますが、その特殊な等価性のため、直接ルックアップしようとすると失敗し、結果としてゼロ値のreflect.Valueが返され、それが<nil>としてフォーマットされるという挙動が確認されています。この挙動を明示的にテストすることで、開発者はこの特定の出力形式を認識し、依存できるようになります。

前提知識の解説

1. Go言語のfmtパッケージ

fmtパッケージは、Go言語におけるフォーマットI/Oを実装するためのパッケージです。C言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換して出力したり、文字列からデータを読み込んだりすることができます。

  • fmt.Sprintf: フォーマットされた文字列を返します。
  • %vバーブ: 値のデフォルトフォーマットで出力します。構造体、マップ、スライスなど、あらゆる型の値を表示する際に便利です。マップの場合、キーと値のペアがmap[key:value key:value]のような形式で出力されます。

2. 浮動小数点数とNaN (Not a Number)

  • 浮動小数点数: コンピュータで実数を近似的に表現するための形式です。Go言語ではfloat32float64があります。
  • IEEE 754標準: 浮動小数点数の表現と演算に関する国際標準です。NaN+Inf(正の無限大)、-Inf(負の無限大)などの特殊な値も定義しています。
  • NaN (Not a Number): 数値ではない結果を表す特殊な浮動小数点値です。例えば、0.0 / 0.0math.Sqrt(-1.0)のような演算結果として生成されます。
  • NaNの特性: IEEE 754標準において、NaNはそれ自身を含め、いかなる値とも等しくありません。つまり、NaN == NaNは常にfalseになります。これは、通常の数値の比較とは異なる非常に重要な特性です。Go言語ではmath.NaN()関数でNaN値を取得できます。

3. Go言語のマップ (Map)

  • Go言語のマップは、キーと値のペアを格納するハッシュテーブルの実装です。
  • キーの要件: マップのキーとして使用できる型は、等価性(==演算子で比較可能であること)が定義されている型に限られます。これは、マップがキーのハッシュ値を計算し、そのハッシュ値とキーの等価性比較によって要素を効率的に検索・挿入・削除するためです。
  • NaNとマップのキー: float64型は等価性が定義されているため、マップのキーとして使用できます。しかし、NaN == NaNfalseであるという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>]"},

このコメントとテストケースから、以下の技術的詳細が読み取れます。

  1. NaNはマップのキーになり得る: map[float64]int{math.NaN(): 1, math.NaN(): 2}という記述から、float64型のNaNがGoのマップのキーとして有効であることが確認できます。Goのマップは、NaNの特殊な等価性(NaN == NaNfalse)のため、異なるNaN値を異なるキーとして扱います。したがって、math.NaN(): 1math.NaN(): 2は、たとえ同じmath.NaN()関数から生成されたものであっても、マップ内では異なるキーとして認識され、両方がマップに格納されます。

  2. fmtパッケージのマップ出力メカニズム: fmtパッケージがマップをフォーマットする際、内部的には以下の手順を踏みます。

    • まず、マップからすべてのキーのリストを取得します。
    • 次に、取得した各キーに対して、マップから対応する値をルックアップします。
  3. NaNキーのルックアップの失敗とreflect.Valueのゼロ値: ここがこのテストの核心です。

    • NaNはマップのキーとして存在しますが、その特殊な等価性のため、fmtパッケージがキーのリストを取得した後、そのキーを使って再度マップから値を「直接フェッチ」しようとすると、内部的なルックアップメカニズムがNaNの等価性ルールに阻まれ、期待通りに値を取得できない場合があります。
    • この「ルックアップの失敗」の結果、reflectパッケージを通じて値を取得しようとした際に、有効なreflect.Valueではなく、その型のゼロ値(この場合はreflect.Valueのゼロ値)が返されます。
    • reflect.Valueのゼロ値は、fmtパッケージによって文字列にフォーマットされると、<nil>として表示されます。これは、ポインタやインターフェースのゼロ値がnilとして表示されるのと同様の挙動です。
  4. テストの目的: コメントにあるように、このテストの目的は「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 == NaNfalseであるため、Goのマップはこれら2つのNaNを異なるキーとして扱います。したがって、このマップには2つのエントリが含まれます。
  • "map[NaN:<nil> NaN:<nil>]": これは、上記のマップを%vでフォーマットした際に期待される出力文字列です。
    • map[...]はマップの標準的な出力形式です。
    • NaNはキーとしてそのまま表示されます。
    • <nil>は、対応する値が表示されるべき場所です。コメントで説明されているように、fmtパッケージがマップのキーを列挙し、その後各キーに対応する値をルックアップしようとすると、NaNの特殊な等価性のためルックアップが失敗し、結果としてreflect.Valueのゼロ値が返されます。このゼロ値がfmtによって<nil>としてフォーマットされるため、このような出力になります。

このテストケースは、fmtパッケージがNaNをキーに持つマップをどのように処理し、どのような出力を生成するかという、特定の(そしてやや複雑な)挙動を明確に定義し、検証しています。これにより、この挙動が将来の変更によって意図せず変更されることを防ぎ、開発者がこの出力に依存できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (fmt, math, reflectパッケージ)
  • IEEE 754浮動小数点標準に関する一般的な情報源
  • Go言語のマップの動作に関する一般的な情報源
  • Go言語のコミット履歴と関連するコードレビュー (Go CL 5564063)