[インデックス 18678] ファイルの概要
このコミットは、Go言語の公式フォーマッタであるgofmt
ツールの内部的なクリーンアップと、reflect
パッケージのAPI変更への対応を目的としています。具体的には、src/cmd/gofmt/rewrite.go
ファイルが変更されており、gofmt
がGoの抽象構文木(AST)を操作する際に使用するリフレクション関連のコードが更新されています。
コミット
commit e9ee0bf63c35c810afb1d820b04d4f41b2f4ff8b
Author: Robert Griesemer <gri@golang.org>
Date: Thu Feb 27 09:00:27 2014 -0800
cmd/gofmt: minor internal cleanups
Reflect changes of reflect API.
LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/69240044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e9ee0bf63c35c810afb1d820b04d4f41b2f4ff8b
元コミット内容
cmd/gofmt: minor internal cleanups
Reflect changes of reflect API.
LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/69240044
変更の背景
このコミットの主な背景は、Go言語の標準ライブラリであるreflect
パッケージのAPI変更にgofmt
が対応する必要があったことです。Go言語は継続的に進化しており、特に初期のバージョンではAPIの変更が頻繁に行われていました。reflect
パッケージは、実行時に型情報を検査したり、値の操作を行ったりするための強力な機能を提供しますが、そのAPIは安定化するまでにいくつかの変更がありました。
このコミットが行われた2014年2月頃は、Go 1.2がリリースされた直後であり、Go 1.3の開発が進められていた時期にあたります。reflect
パッケージのValue.SetValue
メソッドは、Go 1.2で非推奨となり、Go 1.3で削除される予定でした。代わりにValue.Set
メソッドが導入され、より安全で一貫性のある値の設定方法が提供されました。
gofmt
はGoのコードを解析し、抽象構文木(AST)を操作することで整形を行います。このASTの操作には、リフレクションが内部的に利用されていました。そのため、reflect
パッケージのAPI変更、特にSetValue
からSet
への移行は、gofmt
のコードベースに直接的な影響を与えました。このコミットは、このAPI変更に追従し、gofmt
の内部コードを最新のreflect
APIに適合させるためのものです。
前提知識の解説
Go言語のreflect
パッケージ
reflect
パッケージは、Goプログラムが実行時に自身の構造を検査(introspection)し、変更(manipulation)するための機能を提供します。これにより、型情報(reflect.Type
)や値(reflect.Value
)を動的に扱うことができます。
reflect.Type
: Goの型を表します。例えば、int
、string
、struct{}
などの型情報を含みます。reflect.Value
: Goの変数の実行時の値を表します。reflect.Value
を通じて、変数の値を取得したり、設定したりすることができます。
このコミットに関連する重要な変更点は、reflect.Value
の値を設定するメソッドです。
Value.SetValue(x reflect.Value)
(非推奨/削除): Go 1.2で非推奨となり、Go 1.3で削除されたメソッドです。このメソッドは、Value
が指す変数の値をx
の値に設定しようとしました。しかし、このメソッドはいくつかの安全性の問題や、CanSet
のようなプロパティとの整合性の問題がありました。Value.Set(x reflect.Value)
(新しいメソッド):SetValue
に代わって導入されたメソッドです。Set
メソッドは、Value
が指す変数の値をx
の値に設定します。Set
を使用する前に、Value.CanSet()
を呼び出して、そのValue
が設定可能であるか(つまり、エクスポートされたフィールドやポインタが指す値であるか)を確認する必要があります。これにより、より安全で予測可能なリフレクションによる値の操作が可能になりました。
Go言語のast
パッケージ
ast
パッケージは、Goプログラムの抽象構文木(Abstract Syntax Tree)を表現するためのデータ構造と関数を提供します。Goのソースコードは、字句解析(lexing)と構文解析(parsing)を経て、ast
パッケージで定義された構造体のツリーとして表現されます。
ast.Node
: ASTのすべてのノードが実装するインターフェースです。ast.Expr
: 式を表すノードのインターフェースです。ast.File
: 単一のGoソースファイル全体のASTを表す構造体です。
gofmt
のようなツールは、このASTを読み込み、必要に応じてノードを追加、削除、変更することで、コードの整形や変換を行います。
gofmt
ツール
gofmt
は、Go言語のソースコードを標準的なスタイルに自動的に整形するツールです。Go言語のコードベース全体で一貫したフォーマットを強制することで、可読性を高め、コードレビューの負担を軽減します。gofmt
は内部的にGoのparser
パッケージでソースコードをASTに変換し、そのASTを操作して整形後のコードを生成します。このAST操作の過程で、リフレクションが利用されることがあります。
技術的詳細
このコミットは、src/cmd/gofmt/rewrite.go
ファイル内のリフレクションを用いたAST操作ロジックを更新しています。主な変更点は以下の通りです。
-
ast.Print
の引数変更: コメントアウトされたdump
関数内で、ast.Print(fset, val.Interface())
がast.Print(fileSet, val.Interface())
に変更されています。これは、fset
というローカル変数名がfileSet
というグローバル変数名(またはより広いスコープの変数名)に統一されたことを示唆しています。これは直接的なreflect
APIの変更とは関係ありませんが、内部的なクリーンアップの一環です。 -
匿名関数の変数名変更:
rewriteFile
関数内で定義されている匿名関数f
の変数名がrewriteVal
に変更されています。これは、関数の目的をより明確にするためのリファクタリングです。これに伴い、f
が呼び出されていた箇所もrewriteVal
に更新されています。 -
setValue
関数のset
へのリネームとロジック変更: 最も重要な変更点です。setValue
という関数がset
にリネームされ、その内部ロジックがreflect.Value.SetValue
からreflect.Value.Set
の使用に切り替わっています。-
旧
setValue
:func setValue(x, y reflect.Value) { // don't bother if y is invalid to start with if !y.IsValid() { return } defer func() { if r := recover(); r != nil { // ignore panics from x.SetValue(y) } }() x.SetValue(y) }
この関数は、
x.SetValue(y)
を直接呼び出しており、SetValue
がパニックを起こす可能性があるため、defer
とrecover
を使ってパニックを捕捉していました。また、y
が有効でない場合に早期リターンしていました。 -
新
set
:func set(x, y reflect.Value) { // don't bother if x cannot be set or y is invalid if !x.CanSet() || !y.IsValid() { return } defer func() { if r := recover(); r != nil { // ignore panics from x.Set(y) } }() x.Set(y) }
新しい
set
関数では、x.SetValue(y)
がx.Set(y)
に置き換えられています。さらに、x.CanSet()
というチェックが追加されています。これは、Set
メソッドが呼び出される前に、reflect.Value
が設定可能であるか(つまり、エクスポートされたフィールドやポインタが指す値であるか)を確認するためのGoリフレクションのベストプラクティスです。CanSet()
がfalse
の場合、Set
を呼び出すとパニックが発生するため、このチェックは重要です。これにより、パニックを捕捉するdefer/recover
ブロックの必要性が減りますが、念のため残されています。
-
-
apply
関数内のsetValue
呼び出しのset
への変更:apply
関数は、ASTのノードを再帰的に走査し、f
(新しいrewriteVal
)関数を適用する汎用的なヘルパー関数です。この関数内で、reflect.Slice
、reflect.Struct
、reflect.Interface
の各ケースで、要素の値を設定するためにsetValue
が呼び出されていました。これらの呼び出しがすべて新しいset
関数に置き換えられています。
これらの変更は、gofmt
がGoのreflect
パッケージの最新かつ推奨されるAPIを使用するように更新されたことを示しています。これにより、将来のGoバージョンの互換性が確保され、リフレクション操作の堅牢性が向上します。
コアとなるコードの変更箇所
--- a/src/cmd/gofmt/rewrite.go
+++ b/src/cmd/gofmt/rewrite.go
@@ -48,7 +48,7 @@ func parseExpr(s, what string) ast.Expr {
/*
func dump(msg string, val reflect.Value) {
fmt.Printf("%s:\\n", msg)
- ast.Print(fset, val.Interface())
+ ast.Print(fileSet, val.Interface())
fmt.Println()
}
*/
@@ -59,8 +59,9 @@ func rewriteFile(pattern, replace ast.Expr, p *ast.File) *ast.File {\
m := make(map[string]reflect.Value)
pat := reflect.ValueOf(pattern)
repl := reflect.ValueOf(replace)
- var f func(val reflect.Value) reflect.Value // f is recursive
- f = func(val reflect.Value) reflect.Value {\
+
+ var rewriteVal func(val reflect.Value) reflect.Value
+ rewriteVal = func(val reflect.Value) reflect.Value {\
// don't bother if val is invalid to start with
if !val.IsValid() {\
return reflect.Value{}\
@@ -68,22 +69,22 @@ func rewriteFile(pattern, replace ast.Expr, p *ast.File) *ast.File {\
for k := range m {\
delete(m, k)\
}\
- val = apply(f, val)\
+ val = apply(rewriteVal, val)\
if match(m, pat, val) {\
val = subst(m, repl, reflect.ValueOf(val.Interface().(ast.Node).Pos()))\
}\
return val\
}\
-\tr := apply(f, reflect.ValueOf(p)).Interface().(*ast.File)\
+\tr := apply(rewriteVal, reflect.ValueOf(p)).Interface().(*ast.File)\
\tr.Comments = cmap.Filter(r).Comments() // recreate comments list\
\treturn r\
}\
\n-// setValue is a wrapper for x.SetValue(y); it protects\n-// the caller from panics if x cannot be changed to y.\n-func setValue(x, y reflect.Value) {\n-// don't bother if y is invalid to start with\n- if !y.IsValid() {\n+// set is a wrapper for x.Set(y); it protects the caller from panics if x cannot be changed to y.\n+func set(x, y reflect.Value) {\n+// don't bother if x cannot be set or y is invalid\n+ if !x.CanSet() || !y.IsValid() {\
\t\treturn\n \t}\n \tdefer func() {\
@@ -134,16 +135,16 @@ func apply(f func(reflect.Value) reflect.Value, val reflect.Value) reflect.Value\
\tcase reflect.Slice:\
\t\tfor i := 0; i < v.Len(); i++ {\
\t\t\te := v.Index(i)\
-\t\t\tsetValue(e, f(e))\
+\t\t\tset(e, f(e))\
\t\t}\
\tcase reflect.Struct:\
\t\tfor i := 0; i < v.NumField(); i++ {\
\t\t\te := v.Field(i)\
-\t\t\tsetValue(e, f(e))\
+\t\t\tset(e, f(e))\
\t\t}\
\tcase reflect.Interface:\
\t\te := v.Elem()\
-\t\tsetValue(v, f(e))\
+\t\tset(v, f(e))\
\t}\
\treturn val\
}\
コアとなるコードの解説
1. ast.Print
の引数変更
- ast.Print(fset, val.Interface())
+ ast.Print(fileSet, val.Interface())
これはコメントアウトされたコードブロック内の変更ですが、ast.Print
関数に渡すファイルセットの変数がfset
からfileSet
に変更されています。これは、おそらくgofmt
内部でファイルセットを管理する変数名が統一されたことによるものです。機能的な変更はありませんが、コードの一貫性を高めるためのクリーンアップです。
2. 匿名関数の変数名変更
- var f func(val reflect.Value) reflect.Value // f is recursive
- f = func(val reflect.Value) reflect.Value {
+ var rewriteVal func(val reflect.Value) reflect.Value
+ rewriteVal = func(val reflect.Value) reflect.Value {
rewriteFile
関数内で定義されていた再帰的な匿名関数f
がrewriteVal
というより意味のある名前に変更されました。これに伴い、この関数が呼び出されていた箇所も変更されています。
- val = apply(f, val)
+ val = apply(rewriteVal, val)
- r := apply(f, reflect.ValueOf(p)).Interface().(*ast.File)
+ r := apply(rewriteVal, reflect.ValueOf(p)).Interface().(*ast.File)
これらの変更は、コードの可読性と保守性を向上させるためのリファクタリングです。
3. setValue
関数のset
へのリネームとロジック変更
-// setValue is a wrapper for x.SetValue(y); it protects
-// the caller from panics if x cannot be changed to y.
-func setValue(x, y reflect.Value) {
-// don't bother if y is invalid to start with
- if !y.IsValid() {
+// set is a wrapper for x.Set(y); it protects the caller from panics if x cannot be changed to y.
+func set(x, y reflect.Value) {
+// don't bother if x cannot be set or y is invalid
+ if !x.CanSet() || !y.IsValid() {
return
}
defer func() {
if r := recover(); r != nil {
- // ignore panics from x.SetValue(y)
+ // ignore panics from x.Set(y)
}
}()
- x.SetValue(y)
+ x.Set(y)
}
この変更がこのコミットの核心です。
- 関数名のリネーム:
setValue
からset
へ。これは、Goのreflect.Value
の新しいメソッド名Set
に合わせたものです。 - コメントの更新: 関数名の変更に合わせて、コメントも
x.SetValue(y)
からx.Set(y)
に変更されています。 - 値設定ロジックの変更:
- 旧バージョンでは
x.SetValue(y)
を直接呼び出していました。 - 新バージョンでは
x.Set(y)
を呼び出す前に、!x.CanSet()
というチェックが追加されています。reflect.Value.CanSet()
は、そのValue
が設定可能であるかどうか(つまり、エクスポートされたフィールドやポインタが指す値であるか)を返します。CanSet()
がfalse
のValue
に対してSet
を呼び出すとパニックが発生するため、このチェックは非常に重要です。これにより、リフレクションによる値の設定がより安全に行われるようになります。
- 旧バージョンでは
- 早期リターン条件の変更:
- 旧バージョンでは
!y.IsValid()
(設定しようとする値y
が無効な場合)のみをチェックしていました。 - 新バージョンでは
!x.CanSet() || !y.IsValid()
となり、x
が設定可能でない場合も早期リターンするようになりました。
- 旧バージョンでは
4. apply
関数内のsetValue
呼び出しのset
への変更
case reflect.Slice:
for i := 0; i < v.Len(); i++ {
e := v.Index(i)
- setValue(e, f(e))
+ set(e, f(e))
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
e := v.Field(i)
- setValue(e, f(e))
+ set(e, f(e))
}
case reflect.Interface:
e := v.Elem()
- setValue(v, f(e))
+ set(v, f(e))
apply
関数は、ASTの各ノードを走査し、再帰的にrewriteVal
関数を適用する役割を担っています。この関数内で、スライス、構造体、インターフェースの各要素やフィールドの値を更新する際に、以前はsetValue
関数が使われていました。これらの呼び出しがすべて新しいset
関数に置き換えられています。これにより、apply
関数を通じて行われるすべての値設定操作が、新しいreflect.Value.Set
APIとCanSet
チェックの恩恵を受けるようになります。
これらの変更は、Goのreflect
パッケージの進化にgofmt
が追従し、より堅牢で安全なリフレクション操作を行うように更新されたことを明確に示しています。
関連リンク
- Go CL 69240044: https://golang.org/cl/69240044
参考にした情報源リンク
- Go 1.2 Release Notes: https://golang.org/doc/go1.2
- Go 1.3 Release Notes: https://golang.org/doc/go1.3
reflect
パッケージのドキュメント (Go 1.2): https://pkg.go.dev/reflect@go1.2reflect
パッケージのドキュメント (Go 1.3): https://pkg.go.dev/reflect@go1.3- Goの
reflect
パッケージに関する議論や変更履歴 (例: Go issue tracker, mailing listsなど)reflect: Value.Set
issue: https://github.com/golang/go/issues/5976 (これはSet
の導入に関する議論の例であり、直接このコミットの背景ではないかもしれませんが、関連する情報源として挙げられます)
gofmt
のソースコード: https://github.com/golang/go/tree/master/src/cmd/gofmt