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

[インデックス 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メソッドに変更を加えています。

  1. エクスポートされていないフィールドのスキップ:

    • reflect.Structのケースにおいて、構造体のフィールドを走査する際に、IsExported(name)関数(go/astパッケージ内で定義されている)を使用して、フィールド名がエクスポートされているかどうかをチェックするようになりました。
    • エクスポートされていないフィールドは、reflectパッケージの制約により値にアクセスできないため、出力から除外されます。これにより、クラッシュを防ぎます。
  2. 配列のサポート追加:

    • reflect.Arrayの新しいケースがprinter.printメソッドに追加されました。
    • これにより、配列の内容も適切に整形されて出力されるようになります。各要素はインデックスとともに表示されます。
  3. 空のコレクションのコンパクトな出力:

    • reflect.Mapreflect.Arrayreflect.Slicereflect.Structの各ケースにおいて、要素が空の場合(Len() == 0またはフィールドがない場合)、出力が1行にまとめられるようになりました。
    • 例えば、以前は空のマップが複数行にわたって表示されていたのが、map[string]int (len = 0) {}のように簡潔に表示されます。これにより、出力の密度が向上し、可読性が高まります。
  4. テストケースの追加:

    • 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の変更点

  1. Fprint関数のコメント更新: Fprint関数のドキュメンテーションコメントが更新され、「エクスポートされていない構造体フィールドは決して出力されない」という重要な情報が追加されました。これは、リフレクションによるアクセス制限を明確にするものです。

  2. reflect.Mapの出力ロジック変更: 以前は、マップが空であっても改行とインデントが追加されていました。変更後、if x.Len() > 0という条件が追加され、マップに要素がある場合にのみ改行とインデントが行われるようになりました。これにより、空のマップはmap[Type]Type (len = 0) {}のように1行で出力され、よりコンパクトになります。

  3. reflect.Arrayケースの追加: switch x.Kind()の中にcase reflect.Array:が新しく追加されました。これにより、Goの配列型がast.Printで適切に処理されるようになります。マップやスライスと同様に、配列が空の場合は1行で、要素がある場合はインデントされて各要素がインデックスとともに表示されます。

  4. reflect.Sliceの出力ロジック変更: reflect.Mapと同様に、スライスもif x.Len() > 0の条件が追加され、空のスライスが[]Type (len = 0) {}のように1行で出力されるようになりました。

  5. 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の新しい動作、特にエクスポートされていないフィールドのスキップと、空のコレクションおよび配列の新しい出力フォーマットが期待通りに機能することを保証します。

関連リンク

参考にした情報源リンク