[インデックス 13558] ファイルの概要
このコミットは、Go言語のgo/ast
パッケージにおけるast.Print
関数の堅牢性と出力フォーマットを改善するものです。具体的には、エクスポートされていない(unexported)構造体フィールドが原因でast.Print
がクラッシュする問題を修正し、同時に配列のサポートを追加し、空のマップ、配列、スライス、構造体の出力形式をより簡潔にする変更が含まれています。
コミット
- コミットハッシュ:
593c51cff13339c10e9e767209b699eb4ba56c44
- Author: Robert Griesemer gri@golang.org
- Date: Thu Aug 2 17:05:51 2012 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/593c51cff13339c10e9e767209b699eb4ba56c44
元コミット内容
go/ast: ast.Print must not crash with unexported fields
Don't print unexported struct fields; their values are
not accessible via reflection.
Fixes #3898.
Also:
- added support for arrays
- print empty maps, arrays, slices, structs on one line
for a denser output
- added respective test cases
R=r
CC=golang-dev
https://golang.org/cl/6454089
変更の背景
このコミットの主な背景は、Go言語のgo/ast
パッケージが提供するast.Print
(または内部的に使用されるast.Fprint
)関数が、エクスポートされていない構造体フィールドを含む抽象構文木(AST)を処理する際にクラッシュする可能性があったことです。これは、Goのreflect
パッケージの制約に起因します。reflect
パッケージは、エクスポートされていないフィールドの値に直接アクセスすることを許可していません。ast.Print
がASTの構造を再帰的に走査し、その内容を整形して出力しようとする際、エクスポートされていないフィールドにアクセスしようとすると、リフレクションの制約によりランタイムパニックが発生する可能性がありました。
この問題はGoのIssue #3898として報告されており、*ast.Object
が任意のデータを含み、ast.Fprint
が任意のreflect.Value
に対してInterface()
を呼び出そうとすると、誤動作を引き起こすことが指摘されていました。
このコミットは、このクラッシュを防ぐための修正を導入するとともに、ast.Print
の機能と出力の利便性を向上させるための追加変更も行っています。具体的には、配列型のサポートの追加と、空のコレクション(マップ、配列、スライス、構造体)をよりコンパクトに表示することで、出力の可読性を高めています。
前提知識の解説
Go言語のgo/ast
パッケージ
go/ast
パッケージは、Goプログラムの抽象構文木(Abstract Syntax Tree, AST)を表現するためのデータ構造と、それらを操作するための関数を提供します。Goのソースコードは、パーサーによって解析され、このAST構造に変換されます。ASTは、プログラムの構造を木構造で表現したもので、コンパイラ、リンター、コードフォーマッター、静的解析ツールなど、Goのコードをプログラム的に扱う多くのツールで利用されます。
ast.Print
(またはast.Fprint
)は、このAST構造の内容を人間が読める形式で出力するためのユーティリティ関数です。デバッグやASTの構造を理解する際に非常に役立ちます。
Go言語のreflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に型情報(型、フィールド、メソッドなど)を検査し、動的に値を操作するための機能を提供します。これにより、ジェネリックなコードや、実行時に型が決定されるような柔軟なプログラムを作成できます。
しかし、reflect
パッケージには重要な制約があります。Go言語では、構造体のフィールドが小文字で始まる場合(例: fieldName
)、それは「エクスポートされていない(unexported)」フィールドと見なされ、そのパッケージ内からのみアクセス可能です。reflect
パッケージを使用しても、別のパッケージからエクスポートされていないフィールドの値に直接アクセスすることはできません。アクセスしようとすると、ランタイムパニックが発生します。
エクスポートされた(Exported)フィールドとエクスポートされていない(Unexported)フィールド
Go言語では、識別子(変数名、関数名、型名、構造体フィールド名など)の最初の文字が大文字で始まる場合、それは「エクスポートされた(exported)」識別子と呼ばれ、そのパッケージの外部からアクセス可能です。一方、小文字で始まる場合、それは「エクスポートされていない(unexported)」識別子と呼ばれ、その識別子が定義されているパッケージ内からのみアクセス可能です。
このアクセス制御は、Goのモジュール性(カプセル化)の重要な側面であり、ライブラリの内部実装の詳細を隠蔽し、安定したAPIを提供するために使用されます。
ast.Fprint
の動作原理
ast.Fprint
は、内部的にリフレクションを使用して、与えられたGoのデータ構造(通常はASTノード)のフィールドを走査し、その値を出力します。この走査中に、構造体の各フィールドに対してリフレクションを介してアクセスを試みます。
技術的詳細
このコミットは、主にsrc/pkg/go/ast/print.go
ファイル内のprinter.print
メソッドに変更を加えています。
-
エクスポートされていないフィールドのスキップ:
reflect.Struct
のケースにおいて、構造体のフィールドを走査する際に、IsExported(name)
関数(go/ast
パッケージ内で定義されている)を使用して、フィールド名がエクスポートされているかどうかをチェックするようになりました。- エクスポートされていないフィールドは、
reflect
パッケージの制約により値にアクセスできないため、出力から除外されます。これにより、クラッシュを防ぎます。
-
配列のサポート追加:
reflect.Array
の新しいケースがprinter.print
メソッドに追加されました。- これにより、配列の内容も適切に整形されて出力されるようになります。各要素はインデックスとともに表示されます。
-
空のコレクションのコンパクトな出力:
reflect.Map
、reflect.Array
、reflect.Slice
、reflect.Struct
の各ケースにおいて、要素が空の場合(Len() == 0
またはフィールドがない場合)、出力が1行にまとめられるようになりました。- 例えば、以前は空のマップが複数行にわたって表示されていたのが、
map[string]int (len = 0) {}
のように簡潔に表示されます。これにより、出力の密度が向上し、可読性が高まります。
-
テストケースの追加:
src/pkg/go/ast/print_test.go
に、新しい動作を検証するためのテストケースが追加されました。- 空のマップ、配列、スライス、構造体のテストケースが含まれており、コンパクトな出力が期待通りに行われることを確認しています。
- エクスポートされていないフィールドを持つ構造体のテストケースも追加され、それらのフィールドが正しくスキップされることを検証しています。
これらの変更により、ast.Print
はより堅牢になり、より多くのGoの型をサポートし、出力がよりユーザーフレンドリーになりました。
コアとなるコードの変更箇所
src/pkg/go/ast/print.go
--- a/src/pkg/go/ast/print.go
+++ b/src/pkg/go/ast/print.go
@@ -34,7 +34,8 @@ func NotNilFilter(_ string, v reflect.Value) bool {
//
// A non-nil FieldFilter f may be provided to control the output:
// struct fields for which f(fieldname, fieldvalue) is true are
-// are printed; all others are filtered from the output.
+// are printed; all others are filtered from the output. Unexported
+// struct fields are never printed.
//
func Fprint(w io.Writer, fset *token.FileSet, x interface{}, f FieldFilter) (err error) {
// setup printer
@@ -145,15 +146,18 @@ func (p *printer) print(x reflect.Value) {
p.print(x.Elem())
case reflect.Map:
- p.printf("%s (len = %d) {\n", x.Type(), x.Len())
- p.indent++
- for _, key := range x.MapKeys() {
- p.print(key)
- p.printf(": ")
- p.print(x.MapIndex(key))
+ p.printf("%s (len = %d) {", x.Type(), x.Len())
+ if x.Len() > 0 {
+ p.indent++
p.printf("\n")
+ for _, key := range x.MapKeys() {
+ p.print(key)
+ p.printf(": ")
+ p.print(x.MapIndex(key))
+ p.printf("\n")
+ }
+ p.indent--
}
- p.indent--
p.printf("}")
case reflect.Ptr:
@@ -169,32 +173,57 @@ func (p *printer) print(x reflect.Value) {
p.print(x.Elem())
}
+ case reflect.Array:
+ p.printf("%s {", x.Type())
+ if x.Len() > 0 {
+ p.indent++
+ p.printf("\n")
+ for i, n := 0, x.Len(); i < n; i++ {
+ p.printf("%d: ", i)
+ p.print(x.Index(i))
+ p.printf("\n")
+ }
+ p.indent--
+ }
+ p.printf("}")
+
case reflect.Slice:
if s, ok := x.Interface().([]byte); ok {
p.printf("%#q", s)
return
}
- p.printf("%s (len = %d) {\n", x.Type(), x.Len())
- p.indent++
- for i, n := 0, x.Len(); i < n; i++ {
- p.printf("%d: ", i)
- p.print(x.Index(i))
+ p.printf("%s (len = %d) {", x.Type(), x.Len())
+ if x.Len() > 0 {
+ p.indent++
p.printf("\n")
+ for i, n := 0, x.Len(); i < n; i++ {
+ p.printf("%d: ", i)
+ p.print(x.Index(i))
+ p.printf("\n")
+ }
+ p.indent--
}
- p.indent--
p.printf("}")
case reflect.Struct:
- p.printf("%s {\n", x.Type())
- p.indent++
t := x.Type()
+ p.printf("%s {", t)
+ p.indent++
+ first := true
for i, n := 0, t.NumField(); i < n; i++ {
- name := t.Field(i).Name
- value := x.Field(i)
- if p.filter == nil || p.filter(name, value) {
- p.printf("%s: ", name)
- p.print(value)
- p.printf("\n")
+ // exclude non-exported fields because their
+ // values cannot be accessed via reflection
+ if name := t.Field(i).Name; IsExported(name) {
+ value := x.Field(i)
+ if p.filter == nil || p.filter(name, value) {
+ if first {
+ p.printf("\n")
+ first = false
+ }
+ p.printf("%s: ", name)
+ p.print(value)
+ p.printf("\n")
+ }
}
}
p.indent--
src/pkg/go/ast/print_test.go
--- a/src/pkg/go/ast/print_test.go
+++ b/src/pkg/go/ast/print_test.go
@@ -23,6 +23,7 @@ var tests = []struct {
{"foobar", "0 \"foobar\""},
// maps
+ {map[Expr]string{}, `0 map[ast.Expr]string (len = 0) {}`},
{map[string]int{"a": 1},
`0 map[string]int (len = 1) {
1 . "a": 1
@@ -31,7 +32,21 @@ var tests = []struct {
// pointers
{new(int), "0 *0"},
+ // arrays
+ {[0]int{}, `0 [0]int {}`},
+ {[3]int{1, 2, 3},
+ `0 [3]int {
+ 1 . 0: 1
+ 2 . 1: 2
+ 3 . 2: 3
+ 4 }`},
+ {[...]int{42},
+ `0 [1]int {
+ 1 . 0: 42
+ 2 }`},
+
// slices
+ {[]int{}, `0 []int (len = 0) {}`},
{[]int{1, 2, 3},
`0 []int (len = 3) {
1 . 0: 1
@@ -40,6 +55,12 @@ var tests = []struct {
4 }`},
// structs
+ {struct{}{}, `0 struct {} {}`},
+ {struct{ x int }{007}, `0 struct { x int } {}`},
+ {struct{ X, y int }{42, 991},
+ `0 struct { X int; y int } {
+ 1 . X: 42
+ 2 }`},
{struct{ X, Y int }{42, 991},
`0 struct { X int; Y int } {
1 . X: 42
コアとなるコードの解説
src/pkg/go/ast/print.go
の変更点
-
Fprint
関数のコメント更新:Fprint
関数のドキュメンテーションコメントが更新され、「エクスポートされていない構造体フィールドは決して出力されない」という重要な情報が追加されました。これは、リフレクションによるアクセス制限を明確にするものです。 -
reflect.Map
の出力ロジック変更: 以前は、マップが空であっても改行とインデントが追加されていました。変更後、if x.Len() > 0
という条件が追加され、マップに要素がある場合にのみ改行とインデントが行われるようになりました。これにより、空のマップはmap[Type]Type (len = 0) {}
のように1行で出力され、よりコンパクトになります。 -
reflect.Array
ケースの追加:switch x.Kind()
の中にcase reflect.Array:
が新しく追加されました。これにより、Goの配列型がast.Print
で適切に処理されるようになります。マップやスライスと同様に、配列が空の場合は1行で、要素がある場合はインデントされて各要素がインデックスとともに表示されます。 -
reflect.Slice
の出力ロジック変更:reflect.Map
と同様に、スライスもif x.Len() > 0
の条件が追加され、空のスライスが[]Type (len = 0) {}
のように1行で出力されるようになりました。 -
reflect.Struct
の出力ロジック変更:- 構造体の出力も、まず
p.printf("%s {", t)
で1行で開始されるようになりました。 - 最も重要な変更は、構造体のフィールドを走査するループ内で、
if name := t.Field(i).Name; IsExported(name)
という条件が追加されたことです。t.Field(i).Name
でフィールド名を取得し、IsExported(name)
関数(go/ast
パッケージ内のヘルパー関数)でそのフィールドがエクスポートされているか(つまり、名前が大文字で始まるか)をチェックします。- このチェックにより、エクスポートされていないフィールドはスキップされ、リフレクションによるアクセスエラーを防ぎます。
first
というブール変数も導入され、最初の出力されるエクスポートされたフィールドの前にのみ改行が挿入されるように制御されています。これにより、構造体にエクスポートされたフィールドがない場合や、最初のフィールドがフィルタリングされた場合でも、不要な改行が挿入されるのを防ぎ、出力がより整形されます。
- 構造体の出力も、まず
src/pkg/go/ast/print_test.go
の変更点
- 新しいテストケースの追加:
map[Expr]string{}
: 空のマップのテスト。[0]int{}
,[3]int{1, 2, 3}
,[...]int{42}
: 配列のテスト。様々なサイズの配列が正しく出力されることを確認。[]int{}
: 空のスライスのテスト。struct{}{}
: 空の構造体のテスト。struct{ x int }{007}
: エクスポートされていないフィールドのみを持つ構造体のテスト。このテストは、x
フィールドが出力されないことを検証します。struct{ X, y int }{42, 991}
: エクスポートされたフィールドとエクスポートされていないフィールドの両方を持つ構造体のテスト。このテストは、X
フィールドのみが出力され、y
フィールドがスキップされることを検証します。
これらのテストケースは、ast.Print
の新しい動作、特にエクスポートされていないフィールドのスキップと、空のコレクションおよび配列の新しい出力フォーマットが期待通りに機能することを保証します。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/593c51cff13339c10e9e767209b699eb4ba56c44
- Go Issue #3898: https://github.com/golang/go/issues/3898 (直接のリンクは提供されていませんが、コミットメッセージに記載されています)
- Go Code Review: https://golang.org/cl/6454089
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGtOlYjcgsx2ejYo2LcaO_hSJE-WpQcHkWXjAzZPHf83vNcObUsLQiPsEtC2sjDyRbCK9W_c1tADi7lIQR_ugNWOL1z2hh4ontxCGq9kX6-jkInUyooLKMbRh2wl2EWKkrA84k= (Go Issue #3898に関する情報)
- Go言語の公式ドキュメント(
go/ast
パッケージ、reflect
パッケージ、エクスポートルールに関する一般的な知識)