[インデックス 13957] ファイルの概要
コミット
commit ded94b7222eb9ffc7c3a46d4420c12f531f63304
Author: Rob Pike <r@golang.org>
Date: Wed Sep 26 20:46:49 2012 +1000
reflect.DeepEqual: rewrite clarification about nil and empty slice.
The previous version was created by an idiot. This time, Rog Peppe
wrote the text. Thanks, Rog.
(== doesn't work on slices in general, so it makes no sense to
talk about in the context of DeepEqual.)
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6566054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ded94b7222eb9ffc7c3a46d4420c12f531f63304
元コミット内容
このコミットは、reflect.DeepEqual
関数のドキュメンテーションコメントにおける、nilスライスと空スライスの扱いに関する記述の修正です。元の記述は以下の通りでした。
// Note: unlike regular ==, DeepEqual distinguishes an empty slice from a nil slice.
変更の背景
このコミットの背景には、Go言語の reflect.DeepEqual
関数が、nilスライスと空スライスをどのように扱うかについてのドキュメンテーションの明確化があります。元のコメントは「通常の ==
とは異なり、DeepEqual
は空スライスとnilスライスを区別する」と述べていましたが、これは ==
演算子がスライスに対して一般的に機能しない(スライスは参照型であり、==
は同じ基底配列を指しているか、かつ同じ長さであるか、または両方がnilであるかを比較するため、要素の比較には使えない)という事実と矛盾していました。
コミットメッセージにある「The previous version was created by an idiot.」という表現は、元の記述が誤解を招くものであったこと、そしてその修正が重要であるという開発者の強い意図を示しています。Rob Pike氏(Go言語の共同開発者の一人)がこの修正を行い、Rog Peppe氏が新しいテキストを作成したことから、このドキュメンテーションの正確性が非常に重視されていたことが伺えます。
DeepEqual
は、Goの組み込みの ==
演算子では比較できない複雑なデータ構造(スライス、マップ、構造体など)の「深い」等価性をチェックするために設計されています。そのため、==
演算子の挙動を DeepEqual
の文脈で引き合いに出すことは、読者に混乱を与える可能性がありました。特に、nilスライスと空スライスの区別はGo言語の重要な概念であり、DeepEqual
がこれらを異なるものとして扱うことは、その「深い」比較の性質上、期待される挙動です。このコミットは、その挙動をより正確かつ簡潔に伝えることを目的としています。
前提知識の解説
Go言語におけるスライス (Slice)
Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、ポインタ、長さ(len)、容量(cap)の3つの要素から構成されます。
- ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
- 長さ (len): スライスに含まれる要素の数。
- 容量 (cap): スライスのポインタから基底配列の終わりまでの要素の数。
スライスは動的なサイズ変更が可能であり、Go言語で最も頻繁に使用されるデータ構造の一つです。
nilスライスと空スライス
Go言語では、nilスライスと空スライスは異なる概念です。
-
nilスライス:
- 宣言されたが初期化されていないスライス(例:
var s []int
)。 - ポインタがnilであり、長さと容量が0です。
s == nil
はtrue
を返します。- nilスライスは、有効なスライスとして扱われ、多くの操作(
len(s)
,cap(s)
,append(s, ...)
など)を安全に行うことができます。
- 宣言されたが初期化されていないスライス(例:
-
空スライス:
- 長さが0のスライス(例:
s := []int{}
またはs := make([]int, 0)
)。 - ポインタはnilではない場合があり、基底配列を指している可能性がありますが、長さは0です。容量は0またはそれ以上です。
s == nil
はfalse
を返します。
- 長さが0のスライス(例:
例:
var nilSlice []int // nilスライス
emptySliceLiteral := []int{} // 空スライス (リテラル)
emptySliceMake := make([]int, 0) // 空スライス (make関数)
fmt.Printf("nilSlice: %v, len: %d, cap: %d, is nil: %t\n", nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil)
// 出力例: nilSlice: [], len: 0, cap: 0, is nil: true
fmt.Printf("emptySliceLiteral: %v, len: %d, cap: %d, is nil: %t\n", emptySliceLiteral, len(emptySliceLiteral), cap(emptySliceLiteral), emptySliceLiteral == nil)
// 出力例: emptySliceLiteral: [], len: 0, cap: 0, is nil: false
fmt.Printf("emptySliceMake: %v, len: %d, cap: %d, is nil: %t\n", emptySliceMake, len(emptySliceMake), cap(emptySliceMake), emptySliceMake == nil)
// 出力例: emptySliceMake: [], len: 0, cap: 0, is nil: false
reflect.DeepEqual
関数
reflect.DeepEqual
は、Go言語の reflect
パッケージが提供する関数で、2つの値が「深い」等価であるかを再帰的にチェックします。これは、組み込みの ==
演算子では比較できない複雑な型(スライス、マップ、構造体、配列)の比較に特に有用です。
DeepEqual
の主な特徴は以下の通りです。
- 再帰的な比較: 構造体のフィールド、配列やスライスの要素、マップのキーと値を再帰的に比較します。
- nilと空の区別: スライス、マップ、チャネルにおいて、nilと空のインスタンスを区別します。例えば、nilスライスと空スライスは
DeepEqual
では等しくないと判断されます。 - 関数の比較: 関数は、両方がnilの場合にのみ等しいと判断されます。
- ポインタのデリファレンス: ポインタが指す値を比較します。
- 循環参照のハンドリング: 再帰的なデータ構造(例: リンクされたリストやグラフ)における循環参照を正しく処理し、無限ループに陥るのを防ぎます。
==
演算子とスライス
Go言語において、==
演算子はスライスに対しては、その要素の値を比較するようには設計されていません。
- 2つのスライスが両方ともnilである場合、
s1 == s2
はtrue
を返します。 - 2つのスライスが同じ基底配列を指し、かつ同じ長さである場合、
s1 == s2
はtrue
を返します。 - それ以外の場合、
s1 == s2
はfalse
を返します。
これは、スライスが参照型であるためです。スライスの内容を比較するには、reflect.DeepEqual
を使用するか、手動で要素をループして比較する必要があります。
技術的詳細
このコミットは、src/pkg/reflect/deepequal.go
ファイル内の DeepEqual
関数のドキュメンテーションコメントを修正しています。具体的には、以下の行が変更されました。
変更前:
// Note: unlike regular ==, DeepEqual distinguishes an empty slice from a nil slice.
変更後:
// An empty slice is not equal to a nil slice.
この変更の技術的なポイントは、Go言語におけるスライスの等価性比較のニュアンスをより正確に反映させることです。
-
==
演算子とスライスの比較の誤解の解消: 元のコメントは「通常の==
とは異なり」という表現を使っていました。しかし、前述の通り、Goのスライスに対する==
演算子は、スライスの内容(要素)を比較するものではありません。これは、スライスが参照型であり、==
はポインタと長さの比較を行うためです。したがって、DeepEqual
がスライスの内容を「深く」比較する際に、==
演算子との比較を持ち出すことは、読者に「==
がスライスの内容を比較できる」という誤解を与える可能性がありました。新しいコメントは、この誤解を完全に排除し、DeepEqual
の挙動を直接的に述べています。 -
nilスライスと空スライスの区別の強調: Go言語では、nilスライスと空スライスはメモリ上の表現が異なり、
DeepEqual
はこの違いを認識します。nilスライスは基底配列へのポインタがnilであり、空スライスは有効な(ただし長さが0の)基底配列を指すことがあります。DeepEqual
は、これらの内部表現の違いを考慮して等価性を判断するため、両者を異なるものとして扱います。新しいコメント「An empty slice is not equal to a nil slice.」は、このDeepEqual
の重要な特性を簡潔かつ明確に述べています。これは、DeepEqual
が「深い」比較を行うというその目的と完全に一致しています。 -
ドキュメンテーションの正確性と簡潔性: Go言語の標準ライブラリのドキュメンテーションは、その正確性と簡潔性が非常に重視されています。この変更は、その原則に則ったものです。冗長な比較表現を削除し、
DeepEqual
の挙動を直接的に記述することで、より理解しやすく、誤解の余地のないドキュメンテーションになっています。
この修正は、コードの機能自体を変更するものではなく、そのドキュメンテーションを改善し、Go言語のセマンティクスに関する一般的な誤解を避けるためのものです。
コアとなるコードの変更箇所
変更は src/pkg/reflect/deepequal.go
ファイルの125行目から126行目にかけてのドキュメンテーションコメントです。
--- a/src/pkg/reflect/deepequal.go
+++ b/src/pkg/reflect/deepequal.go
@@ -125,7 +125,7 @@ func deepValueEqual(v1, v2 Value, visited map[uintptr]*visit, depth int) (b bool
// DeepEqual tests for deep equality. It uses normal == equality where possible
// but will scan members of arrays, slices, maps, and fields of structs. It correctly
// handles recursive types. Functions are equal only if they are both nil.
-// Note: unlike regular ==, DeepEqual distinguishes an empty slice from a nil slice.
+// An empty slice is not equal to a nil slice.
func DeepEqual(a1, a2 interface{}) bool {
if a1 == nil || a2 == nil {
return a1 == a2
具体的には、以下の行が変更されました。
- 変更前:
// Note: unlike regular ==, DeepEqual distinguishes an empty slice from a nil slice.
- 変更後:
// An empty slice is not equal to a nil slice.
コアとなるコードの解説
このコミットで変更されたのは、reflect.DeepEqual
関数のドキュメンテーションコメントのみであり、関数の実際のロジックには一切変更がありません。
変更されたコメントは、DeepEqual
関数の動作に関する重要な特性を説明しています。
func DeepEqual(a1, a2 interface{}) bool
は、Go言語の reflect
パッケージが提供する関数で、2つの任意のインターフェース値 a1
と a2
が「深い」等価であるかを判定します。
コメントの変更は、特にスライスの比較における DeepEqual
の挙動に焦点を当てています。
-
変更前:
// Note: unlike regular ==, DeepEqual distinguishes an empty slice from a nil slice.
このコメントは、DeepEqual
が「通常の==
とは異なり」、空スライスとnilスライスを区別することを述べていました。しかし、Go言語において==
演算子はスライスの内容を比較するものではないため、「通常の==
とは異なり」という表現は誤解を招く可能性がありました。スライスに対する==
は、ポインタと長さが同じであるか、または両方がnilであるかを比較するものであり、要素の深い比較を行うものではありません。 -
変更後:
// An empty slice is not equal to a nil slice.
この新しいコメントは、より簡潔で正確です。DeepEqual
がnilスライスと空スライスを等しくないと判断するという事実を直接的に述べています。これは、DeepEqual
がスライスの内部構造(ポインタがnilであるか否か)を考慮して比較を行うためです。nilスライスはポインタがnilで長さと容量が0ですが、空スライスはポインタがnilではない(有効な基底配列を指す)場合があり、長さは0でも容量は0以上であることがあります。DeepEqual
はこれらの違いを認識し、異なるものとして扱います。
この変更は、DeepEqual
の機能的な側面ではなく、そのドキュメンテーションの正確性と明瞭性を向上させることを目的としています。これにより、開発者が DeepEqual
の挙動、特にnilスライスと空スライスの区別について、より正確に理解できるようになります。
関連リンク
- Go言語の
reflect
パッケージのドキュメンテーション: https://pkg.go.dev/reflect reflect.DeepEqual
関数のドキュメンテーション: https://pkg.go.dev/reflect#DeepEqual- Go言語におけるスライスに関する公式ブログ記事: https://go.dev/blog/slices
- Go言語におけるnilと空のスライスに関する議論(Stack Overflowなど):
参考にした情報源リンク
- Go言語の公式ドキュメンテーション (
reflect
パッケージ、スライスに関するブログ記事) - Stack OverflowなどのGo言語コミュニティでのnilスライスと空スライスの違いに関する議論
- Go言語のソースコード (
src/pkg/reflect/deepequal.go
) - コミットメッセージと関連するGoのコードレビューシステム (Gerrit) の変更リスト (CL):
https://golang.org/cl/6566054
(現在はhttps://go.dev/cl/6566054
にリダイレクトされる)