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

[インデックス 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 == niltrue を返します。
    • nilスライスは、有効なスライスとして扱われ、多くの操作(len(s), cap(s), append(s, ...) など)を安全に行うことができます。
  • 空スライス:

    • 長さが0のスライス(例: s := []int{} または s := make([]int, 0))。
    • ポインタはnilではない場合があり、基底配列を指している可能性がありますが、長さは0です。容量は0またはそれ以上です。
    • s == nilfalse を返します。

例:

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 == s2true を返します。
  • 2つのスライスが同じ基底配列を指し、かつ同じ長さである場合、s1 == s2true を返します。
  • それ以外の場合、s1 == s2false を返します。

これは、スライスが参照型であるためです。スライスの内容を比較するには、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言語におけるスライスの等価性比較のニュアンスをより正確に反映させることです。

  1. == 演算子とスライスの比較の誤解の解消: 元のコメントは「通常の == とは異なり」という表現を使っていました。しかし、前述の通り、Goのスライスに対する == 演算子は、スライスの内容(要素)を比較するものではありません。これは、スライスが参照型であり、== はポインタと長さの比較を行うためです。したがって、DeepEqual がスライスの内容を「深く」比較する際に、== 演算子との比較を持ち出すことは、読者に「== がスライスの内容を比較できる」という誤解を与える可能性がありました。新しいコメントは、この誤解を完全に排除し、DeepEqual の挙動を直接的に述べています。

  2. nilスライスと空スライスの区別の強調: Go言語では、nilスライスと空スライスはメモリ上の表現が異なり、DeepEqual はこの違いを認識します。nilスライスは基底配列へのポインタがnilであり、空スライスは有効な(ただし長さが0の)基底配列を指すことがあります。DeepEqual は、これらの内部表現の違いを考慮して等価性を判断するため、両者を異なるものとして扱います。新しいコメント「An empty slice is not equal to a nil slice.」は、この DeepEqual の重要な特性を簡潔かつ明確に述べています。これは、DeepEqual が「深い」比較を行うというその目的と完全に一致しています。

  3. ドキュメンテーションの正確性と簡潔性: 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つの任意のインターフェース値 a1a2 が「深い」等価であるかを判定します。

コメントの変更は、特にスライスの比較における 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 パッケージ、スライスに関するブログ記事)
  • Stack OverflowなどのGo言語コミュニティでのnilスライスと空スライスの違いに関する議論
  • Go言語のソースコード (src/pkg/reflect/deepequal.go)
  • コミットメッセージと関連するGoのコードレビューシステム (Gerrit) の変更リスト (CL): https://golang.org/cl/6566054 (現在は https://go.dev/cl/6566054 にリダイレクトされる)