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

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

このコミットは、Go言語の標準ライブラリである reflect パッケージ内の DeepEqual 関数の挙動を修正するものです。具体的には、関数型(Func)の値を比較する際に発生していたパニック(runtime panic)を解消し、関数の比較ロジックを改善しています。

変更されたファイルは以下の2つです。

  • src/pkg/reflect/all_test.go: reflect.DeepEqual のテストケースが追加されています。関数型の比較に関する新しいテストが導入され、修正後の DeepEqual の挙動が期待通りであることを検証します。
  • src/pkg/reflect/deepequal.go: reflect.DeepEqual の主要なロジックが記述されているファイルです。このファイルで、関数型の比較に関するパニックを回避し、正しい比較ロジックを実装するための変更が行われています。

コミット

commit 3a1c226a38fd3f93598c3aabc57e3acd4a764bba
Author: Rob Pike <r@golang.org>
Date:   Fri Feb 24 16:25:39 2012 +1100

    reflect.DeepEqual: don't panic comparing functions
    Functions are equal iff they are both nil.
    
    Fixes #3122.
    
    R=golang-dev, dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/5693057

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

https://github.com/golang/go/commit/3a1c226a38fd3f93598c3aabc57e3acd4a764bba

元コミット内容

reflect.DeepEqual: don't panic comparing functions
Functions are equal iff they are both nil.

Fixes #3122.

R=golang-dev, dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5693057

変更の背景

このコミットの背景には、Go言語の reflect パッケージにおける DeepEqual 関数の既存のバグがあります。具体的には、Issue 3122「reflect.DeepEqual panics on function comparisons」で報告された問題に対応しています。

Go言語では、関数はファーストクラスオブジェクトであり、変数に代入したり、引数として渡したり、戻り値として返したりすることができます。しかし、Goの言語仕様では、関数はポインタ型と同様に、nil との比較を除いて直接的な等価性比較(== 演算子)ができません。これは、関数の実体がメモリ上のどこに配置されるか、あるいはクロージャのキャプチャする環境など、その内部状態が複雑であるため、単純な値の比較では意味のある等価性を定義できないためです。

reflect.DeepEqual は、Goの組み込みの == 演算子では比較できない複雑なデータ構造(スライス、マップ、構造体など)の「深い」等価性を再帰的に比較するために設計されています。しかし、関数型を DeepEqual で比較しようとすると、内部的に未定義の操作が行われ、ランタイムパニックが発生するという問題がありました。これは、DeepEqual が関数型を適切に処理するためのロジックを持っていなかったためです。

このパニックは、開発者が意図せず関数を DeepEqual の引数として渡してしまった場合に、アプリケーションがクラッシュする原因となるため、早急な修正が必要とされていました。このコミットは、この問題を解決し、DeepEqual が関数型を安全に処理できるようにすることを目的としています。

前提知識の解説

reflect パッケージ

Go言語の reflect パッケージは、実行時にプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、型情報(Type)、値情報(Value)、メソッド情報などを動的に取得・操作することができます。

  • reflect.Type: Goの型システムにおける型を表します。例えば、intstringstruct{}func() など、あらゆるGoの型が reflect.Type オブジェクトとして表現されます。
  • reflect.Value: Goの変数の実行時の値を表します。reflect.ValueOf(x) を使うと、任意のGoの変数 xreflect.Value を取得できます。この Value オブジェクトを通じて、その値の型(Type())、種類(Kind())、そしてその値自体(Interface()) にアクセスできます。
  • reflect.Kind: reflect.Type が表す型の「種類」を定義する列挙型です。例えば、IntStringStructSliceMap、そしてこのコミットで重要な Func などがあります。

reflect.DeepEqual 関数

reflect.DeepEqual(a1, a2 interface{}) bool は、2つの引数 a1a2 が「深い」等価性を持つかどうかを判定する関数です。

  • 浅い比較 vs. 深い比較:

    • 浅い比較: == 演算子による比較は「浅い比較」です。これは、プリミティブ型(int, string, bool など)やポインタ、チャネル、関数、インターフェース、マップ(キーと値のペアが同じオブジェクトを指しているか)に対しては値の比較を行いますが、スライスや配列、構造体などの複合型に対しては、それらの要素やフィールドの内容までは比較しません。例えば、2つのスライスが同じ要素を持っていても、異なるメモリ領域に存在すれば == では false になります。
    • 深い比較: DeepEqual は、スライス、マップ、構造体などの複合型に対して、その内部の要素やフィールドまで再帰的に比較します。これにより、内容が完全に一致するかどうかを判断できます。
  • 関数の比較における DeepEqual の課題: 前述の通り、Goの関数は nil との比較を除いて == 演算子で直接比較できません。DeepEqual は、内部で reflect.Value を使って値の種類(Kind)を判別し、それぞれの種類に応じた比較ロジックを適用します。関数型(reflect.Func)の場合、以前の実装ではこの比較ロジックが不完全であったため、パニックを引き起こしていました。

Go言語における関数の等価性

Go言語では、関数は参照型の一種と見なすことができます。2つの関数が等しいとされるのは、両方が nil である場合のみです。nil でない関数値は、たとえ同じコードを指していても、異なるクロージャ環境を持つ可能性があるため、== 演算子では比較できません。このコミットの修正は、このGo言語の関数の等価性に関する基本的な原則に則っています。

技術的詳細

このコミットの技術的詳細は、reflect.DeepEqual の内部実装、特に deepValueEqual 関数が reflect.Func 型をどのように処理するかという点に集約されます。

deepValueEqual 関数は、2つの reflect.Value オブジェクト v1v2 を受け取り、それらが深い等価性を持つかどうかを再帰的に判定します。この関数は、v1.Kind() を使って値の種類を判別し、それぞれの種類に応じた比較ロジックに分岐します。

修正前のコードでは、Func 型に対する明示的な case 文が存在せず、おそらく default ケースで valueInterface(v1, false) == valueInterface(v2, false) のような一般的な等価性比較が試みられていたと考えられます。しかし、reflect.Value から関数インターフェースを取得し、それを == で比較しようとすると、Goの言語仕様上、nil でない関数同士の比較は許可されていないため、ランタイムパニック(具体的には「invalid operation: function value == function value」のようなエラー)が発生していました。

このコミットでは、以下の変更によってこの問題を解決しています。

  1. Func 型の明示的なハンドリング: deepValueEqual 関数内に case Func: という新しい分岐が追加されました。これにより、DeepEqual が関数型を検出した際に、専用のロジックで処理できるようになります。
  2. nil 関数の比較ロジック: case Func: ブロック内で、まず v1.IsNil()v2.IsNil() をチェックします。Goの関数は nil との比較のみが許可されているため、両方の関数が nil であれば true を返します。これは、nil 関数は等しいと見なされるというGoの言語仕様に合致しています。
  3. nil でない関数の比較: v1.IsNil() && v2.IsNil()false の場合、つまり少なくともどちらか一方が nil でない関数である場合、DeepEqualfalse を返します。これは、nil でない関数同士は、たとえ同じコードを指していても、Goの言語仕様上「等しい」とは見なされないという原則に基づいています。DeepEqual は、関数の内部状態(クロージャのキャプチャした変数など)を比較するような高度な機能は提供せず、Goの基本的な等価性ルールに従います。コメント // Can't do better than this: は、これ以上深い比較は不可能または意味がないことを示唆しています。

これらの変更により、DeepEqual は関数型を安全に処理できるようになり、パニックを回避しつつ、Goの言語仕様に準拠した関数の等価性比較(両方 nil の場合のみ true)を提供するようになりました。

また、src/pkg/reflect/all_test.go には、この修正を検証するための新しいテストケースが追加されています。具体的には、nil 関数同士の比較、nil 関数と非nil 関数の比較、および非nil 関数同士の比較のテストが含まれており、修正後の DeepEqual が期待通りの結果を返すことを保証しています。

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

src/pkg/reflect/all_test.go

--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -629,6 +629,13 @@ type DeepEqualTest struct {
 	eq   bool
 }
 
+// Simple functions for DeepEqual tests.
+var (
+	fn1 func()             // nil.
+	fn2 func()             // nil.
+	fn3 = func() { fn1() } // Not nil.
+)
+
 var deepEqualTests = []DeepEqualTest{
 	// Equalities
 	{1, 1, true},
@@ -641,6 +648,7 @@ var deepEqualTests = []DeepEqualTest{
 	{Basic{1, 0.5}, Basic{1, 0.5}, true},
 	{error(nil), error(nil), true},
 	{map[int]string{1: "one", 2: "two"}, map[int]string{2: "two", 1: "one"}, true},
+	{fn1, fn2, true},
 
 	// Inequalities
 	{1, 2, false},
@@ -658,6 +666,8 @@ var deepEqualTests = []DeepEqualTest{
 	{map[int]string{2: "two", 1: "one"}, map[int]string{1: "one"}, false},
 	{nil, 1, false},
 	{1, nil, false},
+	{fn1, fn3, false},
+	{fn3, fn3, false},
 
 	// Nil vs empty: not the same.
 	{[]int{}, []int(nil), false},

src/pkg/reflect/deepequal.go

--- a/src/pkg/reflect/deepequal.go
+++ b/src/pkg/reflect/deepequal.go
@@ -108,6 +108,12 @@ func deepValueEqual(v1, v2 Value, visited map[uintptr]*visit, depth int) (b bool
 			}
 		}
 		return true
+	case Func:
+		if v1.IsNil() && v2.IsNil() {
+			return true
+		}
+		// Can't do better than this:
+		return false
 	default:
 		// Normal equality suffices
 		return valueInterface(v1, false) == valueInterface(v2, false)
@@ -117,8 +123,8 @@ 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, and fields of structs. It correctly
-// handles recursive types.
+// 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.
 func DeepEqual(a1, a2 interface{}) bool {
 	if a1 == nil || a2 == nil {
 		return a1 == a2

コアとなるコードの解説

src/pkg/reflect/all_test.go の変更点

このファイルでは、DeepEqual のテストスイート deepEqualTests に、関数型の比較に関する新しいテストケースが追加されています。

  • fn1, fn2, fn3 の定義:

    var (
    	fn1 func()             // nil.
    	fn2 func()             // nil.
    	fn3 = func() { fn1() } // Not nil.
    )
    

    fn1fn2nil の関数変数として宣言されています。fn3 は匿名関数を代入しており、これは nil ではない具体的な関数値です。これらの変数が、様々な関数の比較シナリオをテストするために使用されます。

  • 新しいテストケースの追加:

    • {fn1, fn2, true}: 両方 nil の関数は等しいと判定されるべきです。
    • {fn1, fn3, false}: nil 関数と非nil 関数は等しくないと判定されるべきです。
    • {fn3, fn3, false}: 同じ非nil 関数同士であっても、Goの言語仕様上は等しいと判定されないべきです。これは、関数が参照型であり、nil でない限り == で比較できないという原則に基づいています。DeepEqual はこの原則に従います。

これらのテストケースは、deepequal.go で行われた修正が、関数型の比較において期待通りの挙動を示すことを検証します。

src/pkg/reflect/deepequal.go の変更点

このファイルでは、deepValueEqual 関数と DeepEqual 関数のコメントが変更されています。

  • deepValueEqual 関数の case Func: ブロックの追加:

    	case Func:
    		if v1.IsNil() && v2.IsNil() {
    			return true
    		}
    		// Can't do better than this:
    		return false
    

    これがこのコミットの最も重要な変更点です。

    • case Func:: v1KindFunc(関数型)である場合にこのブロックが実行されます。
    • if v1.IsNil() && v2.IsNil(): reflect.ValueIsNil() メソッドは、その値が nil であるかどうかをチェックします。関数型の場合、これは関数変数が nil であるかどうかを意味します。両方の関数が nil であれば、Goの言語仕様に従って true を返します。
    • return false: 上記の条件が満たされない場合、つまり少なくともどちらか一方が nil でない関数である場合、false を返します。これは、nil でない関数同士は等しいと見なされないというGoの原則を反映しています。コメント // Can't do better than this: は、関数の内部状態を比較して「深い」等価性を判断することは、Goの言語レベルではサポートされておらず、DeepEqual のスコープ外であることを示唆しています。
  • DeepEqual 関数のコメントの更新:

    --- a/src/pkg/reflect/deepequal.go
    +++ b/src/pkg/reflect/deepequal.go
    @@ -117,8 +123,8 @@ 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, and fields of structs. It correctly
    -// handles recursive types.
    +// 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.
     func DeepEqual(a1, a2 interface{}) bool {
     	if a1 == nil || a2 == nil {
     		return a1 == a2
    

    DeepEqual 関数のドキュメンテーションコメントが更新され、maps がスキャン対象に追加されたことと、「Functions are equal only if they are both nil.」 という関数比較の新しいルールが明記されました。これにより、DeepEqual の挙動がより明確になります。

これらの変更により、reflect.DeepEqual は関数型を安全かつGoの言語仕様に準拠した形で比較できるようになり、以前のパニックの問題が解決されました。

関連リンク

参考にした情報源リンク