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

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

このコミットは、Goコンパイラ(cmd/gc)のreflect.cファイルと、関連するテストケースtest/fixedbugs/issue7363.goに影響を与えます。reflect.cはGoのreflectパッケージが型情報をどのように処理し、ランタイムに公開するかを定義するコンパイラの一部です。test/fixedbugs/issue7363.goは、このコミットが修正する特定のバグを検証するための新しいテストファイルです。

コミット

commit a8a7f18aeaf66e74f4f89b95d6cd43bab6cbf59d
Author: Chris Manghane <cmang@golang.org>
Date:   Thu Feb 20 11:32:55 2014 -0800

    cmd/gc: make embedded, unexported fields read-only.
    
    Fixes #7363.
    
    LGTM=gri
    R=gri, rsc, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/66510044

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

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

元コミット内容

cmd/gc: make embedded, unexported fields read-only.

Fixes #7363.

LGTM=gri
R=gri, rsc, bradfitz
CC=golang-codereviews
https://golang.org/cl/66510044

変更の背景

このコミットは、Goのreflectパッケージにおける特定のバグ、Issue 7363を修正するために導入されました。このバグは、埋め込まれた(embedded)かつ非公開(unexported)な構造体フィールドが、reflect.Value.CanSet()メソッドによって誤って書き込み可能であると報告される問題でした。

Goのreflectパッケージは、プログラムの実行中に型情報を検査し、操作するための機能を提供します。しかし、Goの言語仕様では、非公開フィールドはパッケージ外から直接アクセスしたり変更したりすることはできません。これはカプセル化の原則に基づいています。reflectパッケージもこの原則に従うべきであり、非公開フィールドに対してCanSet()trueを返すことは、言語の安全性と整合性に反する動作でした。

特に、埋め込みフィールドはGoのユニークな機能であり、ある構造体が別の構造体のフィールドを「匿名で」含むことを可能にします。これにより、埋め込まれた構造体のメソッドやフィールドが、埋め込み元の構造体のメソッドやフィールドであるかのように直接アクセスできるようになります。しかし、この埋め込みフィールドが非公開である場合、そのフィールド自体は外部からアクセスできないはずです。reflectパッケージがこの制約を正しく反映していなかったため、この修正が必要となりました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とreflectパッケージの知識が必要です。

  1. 構造体(Structs): 複数のフィールドをまとめた複合データ型。
  2. 埋め込みフィールド(Embedded Fields): Goの構造体は、他の構造体を匿名で埋め込むことができます。これにより、埋め込まれた構造体のフィールドやメソッドが、埋め込み元の構造体のフィールドやメソッドであるかのように直接アクセスできるようになります。これは継承に似ていますが、Goでは「コンポジション(合成)」と呼ばれます。 例:
    type Inner struct {
        X int
    }
    type Outer struct {
        Inner // Inner構造体を埋め込み
        Y int
    }
    // Outerのインスタンスから inner.X ではなく outer.X のようにアクセスできる
    o := Outer{Inner: Inner{X: 10}, Y: 20}
    fmt.Println(o.X) // 10
    
  3. 公開(Exported)と非公開(Unexported)識別子: Goでは、識別子(変数名、関数名、型名、フィールド名など)の最初の文字が大文字であれば公開され、小文字であれば非公開となります。公開された識別子はパッケージ外からアクセス可能ですが、非公開の識別子は同じパッケージ内からのみアクセス可能です。 例:
    type MyStruct struct {
        PublicField  int // 公開
        privateField int // 非公開
    }
    
  4. reflectパッケージ: Goのreflectパッケージは、実行時にプログラムの構造を検査(リフレクション)するための機能を提供します。これにより、変数の型、値、構造体のフィールドなどを動的に調べたり、操作したりできます。
    • reflect.Value: Goのあらゆる値のランタイム表現です。
    • reflect.Value.CanSet(): reflect.Valueが表す値が変更可能(settable)であるかどうかを返します。変更可能であるためには、その値がアドレス可能(addressable)であり、かつ公開されている必要があります。非公開フィールドは通常、CanSet()falseを返すべきです。
    • reflect.Value.Elem(): ポインタが指す要素のreflect.Valueを返します。
    • reflect.Value.Field(i): 構造体のi番目のフィールドのreflect.Valueを返します。

このバグは、埋め込まれた非公開フィールドに対してCanSet()が誤ってtrueを返すというものでした。これは、reflectパッケージがGoのアクセス制御ルールを正しく適用していなかったことを意味します。

技術的詳細

このコミットの技術的詳細は、Goコンパイラのreflect.cファイルにおける型情報の処理、特に構造体フィールドの可視性(公開/非公開)と埋め込みの扱いに関するものです。

reflect.cは、Goの型システムがランタイムリフレクションのために必要なメタデータを生成する部分です。dgopkgpathdgostringptrといった関数は、型情報(特にシンボル名やパッケージパス)をランタイムが利用できる形式に変換する役割を担っています。

変更前のコードでは、t1->type->sym->pkg == builtinpkgという条件がありました。これは、型がbuiltinpkg(Goの組み込み型、例えばint, stringなど)に属している場合に特定の処理を行うことを意味します。組み込み型は常に公開されており、そのフィールドも同様に扱われます。

しかし、問題はユーザー定義の型、特に埋め込まれた非公開フィールドの場合に発生しました。変更前のロジックでは、埋め込まれた非公開フィールドがbuiltinpkgに属していない場合、そのフィールドが非公開であるかどうかを適切にチェックしていませんでした。その結果、reflect.Value.CanSet()が、本来falseを返すはずの非公開フィールドに対してtrueを返してしまうことがありました。

このコミットでは、以下の条件が追加されました。 !exportname(t1->type->sym->name)

  • exportname関数は、Goの識別子が大文字で始まる(つまり公開されている)かどうかをチェックするユーティリティ関数です。!exportname(...)は、その識別子が非公開であることを意味します。
  • t1->type->sym->nameは、フィールドのシンボル名を表します。

したがって、追加された条件t1->type->sym->pkg == builtinpkg || !exportname(t1->type->sym->name)は、以下のいずれかの条件が満たされる場合に特定の処理(おそらく、そのフィールドが「特別な」扱いを受けるべきである、またはアクセス制限があることを示すメタデータを生成する)を行うことを意味します。

  1. 型が組み込みパッケージに属している。
  2. 型が組み込みパッケージに属していないが、そのシンボル名が非公開である。

この修正により、reflectパッケージが非公開の埋め込みフィールドを正しく認識し、CanSet()falseを返すように、ランタイムの型情報が適切に生成されるようになりました。これにより、Goの言語仕様におけるアクセス制御の原則がreflectパッケージを介しても維持されることが保証されます。

test/fixedbugs/issue7363.goは、この修正が正しく機能することを確認するためのテストケースです。このテストでは、非公開の埋め込み構造体フィールドを持つ構造体を定義し、reflect.ValueOfCanSet()を使用して、そのフィールドが書き込み可能でないことをアサートしています。もしCanSet()trueを返した場合、テストはパニックを起こし、バグが修正されていないことを示します。

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

src/cmd/gc/reflect.cの以下の部分が変更されました。

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

また、新しいテストファイルtest/fixedbugs/issue7363.goが追加されました。

// run

// Copyright 2014 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// issue 7363: CanSet must return false for unexported embedded struct fields.

package main

import "reflect"

type a struct {
}

type B struct {
	a
}

func main() {
	b := &B{}
	v := reflect.ValueOf(b).Elem().Field(0)
	if v.CanSet() {
		panic("B.a is an unexported embedded struct field")
	}
}

コアとなるコードの解説

src/cmd/gc/reflect.cの変更

このC言語のコードスニペットは、Goコンパイラ(cmd/gc)の一部であり、Goの型システムがリフレクションのために必要なメタデータを生成するロジックを含んでいます。

変更された行は、reflectパッケージが型情報を処理する際に、特定のシンボル(t1->type->sym)がどのように扱われるべきかを決定する条件分岐です。

  • 変更前:

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

    この条件は、「シンボルが存在し、かつそのシンボルが組み込みパッケージ(builtinpkg)に属している場合」に真となります。組み込み型(int, stringなど)は常に公開されており、特別な扱いを受けます。

  • 変更後:

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

    この変更により、条件に|| !exportname(t1->type->sym->name)が追加されました。

    • !exportname(t1->type->sym->name): これは、t1->type->sym->name(シンボル名)が非公開(小文字で始まる)である場合に真となります。
    • したがって、新しい条件は「シンボルが存在し、かつそのシンボルが組み込みパッケージに属しているか、またはそのシンボル名が非公開である場合」に真となります。

この修正の目的は、reflectパッケージが非公開の埋め込みフィールドを正しく認識し、それらが書き込み可能でない(CanSet()falseを返す)ように、ランタイムの型情報を生成することです。以前は、非公開の埋め込みフィールドが組み込み型でない場合、この条件に引っかからず、誤って書き込み可能であると判断される可能性がありました。この修正により、Goのアクセス制御ルールがリフレクションを介しても厳密に適用されるようになりました。

test/fixedbugs/issue7363.goの解説

このGo言語のテストファイルは、上記のreflect.cの変更が意図した通りに機能するかを検証するために追加されました。

  1. type a struct {}: 非公開の構造体aを定義します。Goの命名規則により、小文字で始まるaは非公開です。
  2. type B struct { a }: 構造体Bを定義し、その中に非公開の構造体aを匿名で埋め込みます。
  3. func main(): テストの実行エントリポイントです。
    • b := &B{}: B型のポインタbを作成します。
    • v := reflect.ValueOf(b).Elem().Field(0):
      • reflect.ValueOf(b): ポインタbreflect.Valueを取得します。
      • .Elem(): ポインタが指す要素(この場合はB構造体自体)のreflect.Valueを取得します。
      • .Field(0): B構造体の最初のフィールド(この場合は埋め込まれたa)のreflect.Valueを取得します。
    • if v.CanSet() { panic(...) }: 取得したv(埋め込まれた非公開フィールドaを表すreflect.Value)に対してCanSet()を呼び出します。
      • Goの言語仕様とアクセス制御の原則に基づき、非公開フィールドはパッケージ外から変更できないため、CanSet()falseを返す必要があります。
      • もしCanSet()trueを返した場合、それはバグ(Issue 7363)がまだ存在することを示し、テストはpanicを発生させて失敗します。

このテストは、reflectパッケージが非公開の埋め込みフィールドの書き込み可能性を正しく判断していることを保証するための重要な検証です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントと仕様
  • Goのreflectパッケージに関する一般的な解説記事
  • Goのアクセス修飾子(公開/非公開)に関する解説記事
  • Goの埋め込み(Embedding)に関する解説記事
  • GitHubのgolang/goリポジトリのIssueトラッカー
  • Goのコードレビューシステム(Gerrit)のアーカイブ