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

[インデックス 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の内部コードを最新のreflectAPIに適合させるためのものです。

前提知識の解説

Go言語のreflectパッケージ

reflectパッケージは、Goプログラムが実行時に自身の構造を検査(introspection)し、変更(manipulation)するための機能を提供します。これにより、型情報(reflect.Type)や値(reflect.Value)を動的に扱うことができます。

  • reflect.Type: Goの型を表します。例えば、intstringstruct{}などの型情報を含みます。
  • 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操作ロジックを更新しています。主な変更点は以下の通りです。

  1. ast.Printの引数変更: コメントアウトされたdump関数内で、ast.Print(fset, val.Interface())ast.Print(fileSet, val.Interface())に変更されています。これは、fsetというローカル変数名がfileSetというグローバル変数名(またはより広いスコープの変数名)に統一されたことを示唆しています。これは直接的なreflectAPIの変更とは関係ありませんが、内部的なクリーンアップの一環です。

  2. 匿名関数の変数名変更: rewriteFile関数内で定義されている匿名関数fの変数名がrewriteValに変更されています。これは、関数の目的をより明確にするためのリファクタリングです。これに伴い、fが呼び出されていた箇所もrewriteValに更新されています。

  3. 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がパニックを起こす可能性があるため、deferrecoverを使ってパニックを捕捉していました。また、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ブロックの必要性が減りますが、念のため残されています。

  4. apply関数内のsetValue呼び出しのsetへの変更: apply関数は、ASTのノードを再帰的に走査し、f(新しいrewriteVal)関数を適用する汎用的なヘルパー関数です。この関数内で、reflect.Slicereflect.Structreflect.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関数内で定義されていた再帰的な匿名関数frewriteValというより意味のある名前に変更されました。これに伴い、この関数が呼び出されていた箇所も変更されています。

-		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()falseValueに対して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.SetAPIとCanSetチェックの恩恵を受けるようになります。

これらの変更は、Goのreflectパッケージの進化にgofmtが追従し、より堅牢で安全なリフレクション操作を行うように更新されたことを明確に示しています。

関連リンク

参考にした情報源リンク