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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるリフレクション関連の変更を取り消すものです。具体的には、以前のコミット(CL 66510044 / 6c0339d94123)によって導入された、埋め込み型かつ非公開フィールドのCanSet挙動に関する修正が、他の問題を引き起こしたため元に戻されました。

コミット

commit c48db9a47388bfbcec97d33b03ee81ffe7eb50af
Author: Russ Cox <rsc@golang.org>
Date:   Mon Apr 14 09:48:11 2014 -0400

    undo CL 66510044 / 6c0339d94123
    
    Broke other things - see issue 7522.
    
    Fixes #7522.
    Reopens issue 7363.
    
    ««« original CL description
    cmd/gc: make embedded, unexported fields read-only.
    
    Fixes #7363.
    
    LGTM=gri
    R=gri, rsc, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/66510044
    »»»
    
    LGTM=r, mpvl
    R=golang-codereviews, r
    CC=golang-codereviews, iant, mpvl
    https://golang.org/cl/85580046

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

https://github.com/golang/go/commit/c48db9a47388bfbcec97d33b03ee81ffe7eb50af

元コミット内容

このコミットが取り消している元のコミット(CL 66510044 / 6c0339d94123)の目的は、「cmd/gc: 埋め込み型で非公開のフィールドを読み取り専用にする」ことでした。これは、issue 7363で報告された問題を修正するためのものでした。

変更の背景

このコミットは、以前のコミット(CL 66510044 / 6c0339d94123)がGoのリフレクションシステムにおいて新たな問題(issue 7522)を引き起こしたため、その変更を取り消す目的で作成されました。元のコミットはissue 7363を修正しようとしましたが、その修正が他の予期せぬ副作用をもたらしたため、一度その修正をロールバックし、issue 7363を再オープンする形となりました。これは、複雑なシステムにおける変更が、意図しない連鎖的な影響を及ぼす典型的な例と言えます。

前提知識の解説

Go言語のリフレクション

Go言語のリフレクションは、プログラムの実行中に型情報を検査したり、変数の値を動的に操作したりする機能です。reflectパッケージを通じて提供され、主に以下の主要な型を使用します。

  • reflect.Type: Goの型の情報を表します。例えば、構造体のフィールド名やメソッドなどを取得できます。
  • reflect.Value: Goの変数の値を表します。この値を通じて、変数の内容を読み取ったり、書き換えたりすることができます。

reflect.Value.CanSet()

reflect.Value型にはCanSet()というメソッドがあります。このメソッドは、そのreflect.Valueが表す変数の値を変更できるかどうかを返します。Goのリフレクションのルールとして、以下の条件を満たす場合にのみCanSet()trueを返します。

  1. アドレス可能であること (Addressable): reflect.Valueが、メモリ上の特定のアドレスを持つ変数(例えば、ポインタの参照外しや構造体のフィールドなど)を表している必要があります。一時的な値やマップのキーなどはアドレス可能ではありません。
  2. 非公開フィールドでないこと: 構造体の非公開(unexported)フィールドは、そのフィールドが定義されたパッケージの外からはCanSet()falseを返します。これはカプセル化の原則に基づいています。

埋め込み型 (Embedded Types)

Go言語の構造体は、他の構造体をフィールドとして埋め込むことができます。これにより、埋め込まれた型のメソッドやフィールドが、外側の構造体のメソッドやフィールドであるかのように直接アクセスできるようになります。これは、継承に似た機能を提供しますが、Goでは「コンポジション(合成)」として扱われます。

例:

type Inner struct {
    unexportedField int // 非公開フィールド
    ExportedField   string // 公開フィールド
}

type Outer struct {
    Inner // Inner型を埋め込み
}

この場合、Outer型のインスタンスからunexportedFieldExportedFieldに直接アクセスできますが、unexportedFieldは非公開であるため、パッケージ外からは直接操作できません。

issue 7363 の問題

issue 7363は、「reflect: CanSetが非公開の埋め込み構造体フィールドに対してtrueを返す」という問題でした。これは、reflect.Value.CanSet()のドキュメントに記載されている「非公開の構造体フィールドの使用によって取得されたものでない限り、Valueは変更可能である」というルールに反する挙動でした。つまり、非公開の埋め込みフィールドであっても、リフレクションを通じてCanSet()trueを返してしまい、意図しない書き換えが可能になる可能性があったということです。

技術的詳細

このコミットは、src/cmd/gc/reflect.cというファイルに対する変更を取り消しています。このファイルはGoコンパイラのバックエンドの一部であり、Goの型システムとリフレクションの内部的な挙動を定義しています。特に、Goの型情報がどのようにランタイムに表現され、リフレクションがその情報にどのようにアクセスするかを扱っています。

元のコミット(CL 66510044)では、reflect.c内のdgopkgpathおよびdgostringptr関連のロジックが変更され、埋め込み型で非公開のフィールドに対するCanSetの挙動が修正されました。具体的には、フィールドが非公開であるかどうかのチェックが追加され、非公開フィールドの場合はCanSetfalseを返すように調整されたと考えられます。

しかし、この修正がissue 7522で報告された「他のもの」を壊したため、このコミットではその変更が元に戻されました。これは、コンパイラの内部的な型表現やリフレクションの挙動が非常にデリケートであり、一つの修正が広範囲に影響を及ぼす可能性があることを示しています。

また、test/fixedbugs/issue7363.goというテストファイルが削除されています。これは、元のコミットでissue 7363の修正を検証するために追加されたテストですが、今回のコミットでその修正が取り消されたため、関連するテストも不要となり削除されました。このテストファイルは、reflect.ValueOf(b).Elem().Field(0).CanSet()trueを返した場合にパニックを起こすことで、issue 7363の修正が正しく適用されたことを確認するものでした。

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

src/cmd/gc/reflect.c

--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -1138,8 +1138,7 @@ ok:
 				ot = dgopkgpath(s, ot, t1->sym->pkg);
 			} else {
 				ot = dgostringptr(s, ot, nil);
-				if(t1->type->sym != S &&
-				   (t1->type->sym->pkg == builtinpkg || !exportname(t1->type->sym->name)))
+				if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)
 					ot = dgopkgpath(s, ot, localpkg);
 				else
 					ot = dgostringptr(s, ot, nil);

test/fixedbugs/issue7363.go

このファイルは完全に削除されました。

コアとなるコードの解説

src/cmd/gc/reflect.cの変更は、Goコンパイラがリフレクションの型情報を生成する部分にあります。特に、dgopkgpathdgostringptrは、Goの型がランタイムでどのように表現されるかに関連する関数です。

元の変更では、if文の条件が以下のように変更されていました。

if(t1->type->sym != S &&
   (t1->type->sym->pkg == builtinpkg || !exportname(t1->type->sym->name)))

この条件は、t1->type->symが存在し、かつそのシンボルがbuiltinpkg(組み込みパッケージ)に属するか、またはexportname関数がfalseを返す(つまり、非公開の名前である)場合に特定の処理を行うことを意味していました。これは、非公開フィールドに対するCanSetの挙動を制御しようとした試みと考えられます。

しかし、このコミットではその変更が元に戻され、条件が以下のように簡素化されました。

if(t1->type->sym != S && t1->type->sym->pkg == builtinpkg)

これにより、非公開フィールドに関する特別なチェックが削除され、元の挙動に戻されました。このロールバックは、元の修正がGoのリフレクションシステム全体に予期せぬ影響を与え、他の重要な機能が損なわれたため、一時的に安定性を優先した結果であると推測されます。

test/fixedbugs/issue7363.goの削除は、このコミットがissue 7363の修正を無効にしたことを明確に示しています。このテストは、非公開の埋め込み構造体フィールドに対してreflect.Value.CanSet()trueを返さないことを期待していましたが、修正が取り消されたため、このテストはもはや適切ではなくなりました。

関連リンク

参考にした情報源リンク

  • Go言語のreflectパッケージのドキュメント: https://pkg.go.dev/reflect
  • Go言語の埋め込み型に関するドキュメントやチュートリアル (一般的なGoの学習リソース)
  • Go言語のコンパイラ内部に関する情報 (Goのソースコードや関連する設計ドキュメント)
  • Go言語のIssueトラッカー (GitHub Issues)
  • Go Code Review Comments (Goのコードレビューガイドライン)
  • Go言語のソースコード (特にsrc/cmd/gc/reflect.cおよびsrc/reflect/value.goなど)