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

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

このコミットは、Go言語のドキュメンテーションツールであるgo/docパッケージにおける重要な改善を含んでいます。具体的には、Goの組み込み型であるerrorインターフェースが、ドキュメント生成時にエクスポートされた型と同様に扱われるように変更されました。これにより、errorインターフェースが構造体やインターフェースに埋め込まれている場合に、その存在が適切にドキュメントに反映されるようになります。

また、テストの選択的実行を可能にするために、go/docパッケージのテストスイートに-filesフラグが追加されました。このフラグを使用することで、正規表現に一致するGoテストファイルのみを対象にテストを実行できるようになり、開発時の効率が向上します。

コミット

commit 1076d4ef73a3fd802432b8c54d166d5c562b7d2d
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Feb 14 09:13:12 2012 -0800

    go/doc: treat predeclared error interface like an exported type
    
    Also added -files flag to provide regexp for test files for
    selective testing.
    
    Fixes #2956.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5657045

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

https://github.com/golang/go/commit/1076d4ef73a3fd802432b8c54d166d5c562b7d2d

元コミット内容

go/doc: treat predeclared error interface like an exported typego/doc: 事前宣言されたerrorインターフェースをエクスポートされた型のように扱う)

Also added -files flag to provide regexp for test files for selective testing. (また、選択的テストのためにテストファイル用の正規表現を提供する-filesフラグを追加)

Fixes #2956. (Issue #2956を修正)

変更の背景

このコミットは、主にGo言語のドキュメンテーションツールgo/docの挙動に関する2つの課題に対処しています。

  1. errorインターフェースのドキュメント生成の不整合: Go言語にはerrorという事前宣言されたインターフェースが存在します。これは、エラーハンドリングにおいて非常に重要な役割を果たす組み込み型です。しかし、go/docツールがドキュメントを生成する際、このerrorインターフェースが他のエクスポートされた型(大文字で始まる型)と同様に適切に扱われない場合がありました。特に、構造体やインターフェースの匿名フィールドとしてerrorが埋め込まれている場合、ドキュメントからその情報が欠落したり、意図しないフィルタリングが行われたりする問題がありました。これにより、生成されるドキュメントがコードの実際の構造を正確に反映せず、開発者が混乱する可能性がありました。このコミットは、errorインターフェースをエクスポートされた型と同様に扱い、ドキュメントに適切に表示されるようにすることで、この不整合を解消しようとしています。

  2. テスト実行の効率化: go/docパッケージのテストスイートを実行する際、すべてのテストファイルが常に実行されていました。これは、特定の変更をテストしたい場合や、大規模なテストスイートの一部のみを迅速に実行したい場合に非効率的でした。開発者は、変更に関連するテストのみを実行できるようなメカニズムを求めていました。このコミットでは、テスト実行時に正規表現を用いて対象ファイルをフィルタリングできる-filesフラグを導入することで、この課題を解決し、開発ワークフローの効率化を図っています。

Fixes #2956という記述から、これらの変更がGoのIssueトラッカーに登録されていた特定のバグや改善要求に対応するものであることがわかります。

前提知識の解説

Go言語のerrorインターフェース

Go言語におけるエラーハンドリングは、他の多くの言語とは異なり、例外処理ではなく、errorインターフェースを用いた多値戻り値によって行われます。

  • 定義: errorインターフェースはGoの組み込み型であり、以下のように定義されています。
    type error interface {
        Error() string
    }
    
    このインターフェースは、Error() stringという単一のメソッドを持ちます。
  • 目的: Error()メソッドは、エラーの文字列表現を返すことを目的としています。これにより、エラーが発生した際にその内容をログに出力したり、ユーザーに表示したりすることができます。
  • 利用方法: Goの関数は、エラーが発生する可能性がある場合、通常、最後の戻り値としてerror型を返します。エラーが発生しなかった場合はnilを返します。
    func doSomething() (result string, err error) {
        // ... 処理 ...
        if somethingWentWrong {
            return "", errors.New("something went wrong")
        }
        return "success", nil
    }
    
  • カスタムエラー型: 開発者は、独自のカスタムエラー型を定義することで、より詳細なエラー情報を含めることができます。カスタムエラー型は、errorインターフェースを実装(Error() stringメソッドを定義)していれば、error型として扱うことができます。
  • 事前宣言された型: errorはGo言語に最初から組み込まれている「事前宣言された型」の一つです。これは、int, string, boolなどと同様に、特別な意味を持つ型として扱われます。

Go言語のgo/docパッケージ

go/docパッケージは、Go言語のソースコードからドキュメンテーションを生成するためのツール群を提供する標準ライブラリの一部です。

  • 目的: go/docパッケージは、Goのソースコード内のコメント(特にトップレベルの宣言の直前にあるコメント)を解析し、構造化されたドキュメンテーションデータを生成します。このデータは、go docコマンドやgodocツール(pkg.go.devのようなオンラインドキュメントサイトの基盤)によって利用され、開発者がGoのパッケージやシンボルの使い方を理解するのに役立ちます。
  • ドキュメンテーションコメント: Goでは、エクスポートされた(大文字で始まる)パッケージ、定数、関数、型、変数の直前に記述されたコメントがドキュメンテーションコメントとして認識されます。これらのコメントは、go/docによって解析され、生成されるドキュメントに含まれます。
  • 抽象構文木 (AST): go/docパッケージは、go/parserパッケージを使用してGoのソースコードを解析し、抽象構文木(AST)を構築します。ASTは、ソースコードの構造をツリー形式で表現したもので、go/docはこのASTを走査してドキュメンテーションに必要な情報を抽出します。
  • エクスポートされたシンボル: Goのドキュメンテーションツールは、通常、エクスポートされた(外部からアクセス可能な)シンボルのみをドキュメントに含めます。これは、内部実装の詳細を隠蔽し、公開APIのみを明確にするためです。

Go言語のテストフレームワークとgo testコマンド

Go言語には、標準でテストフレームワークが組み込まれており、go testコマンドを使用してテストを実行します。

  • テストファイルの命名規則: テストファイルは、通常、テスト対象のファイルと同じディレクトリに配置され、ファイル名の末尾に_test.goを付けます(例: my_package.goに対するmy_package_test.go)。
  • テスト関数の命名規則: テスト関数はTestで始まり、その後に大文字で始まる名前が続きます(例: func TestMyFunction(t *testing.T))。
  • go testコマンド: go testコマンドは、現在のディレクトリまたは指定されたパッケージ内のすべてのテストファイルを見つけて実行します。
  • フラグ: go testコマンドには、テストの挙動を制御するための様々なフラグが用意されています。例えば、-vフラグは詳細なテスト結果を表示し、-runフラグは特定のテスト関数のみを実行するための正規表現を指定できます。

技術的詳細

このコミットの技術的詳細は、go/docパッケージがGoのASTをどのように処理し、特にerrorインターフェースのような事前宣言された型や、匿名フィールドの扱いをどのように改善したかに焦点を当てています。

errorインターフェースの特殊な扱い

go/docは、通常、エクスポートされていない(小文字で始まる)フィールドやメソッドをドキュメントからフィルタリングします。しかし、errorインターフェースは、その名前が小文字で始まるにもかかわらず、Go言語のコアな機能であり、しばしば匿名フィールドとして構造体やインターフェースに埋め込まれます。このコミット以前は、go/docerrorインターフェースを単なるエクスポートされていないフィールドとして扱ってしまい、ドキュメントから除外してしまう問題がありました。

この変更では、src/pkg/go/doc/exports.go内のfilterFieldList関数にロジックが追加されました。この関数は、構造体やインターフェースのフィールドをフィルタリングする役割を担っています。変更後、匿名フィールドの名前が"error"である場合、それが事前宣言されたerrorインターフェースである可能性を考慮し、一時的にkeepField = trueとして保持するようになりました。

さらに、src/pkg/go/doc/reader.goには、errorDeclという新しいフィールドとremember関数が追加されました。

  • errorDeclは、パッケージ内でerrorという名前の型がローカルに宣言されているかどうかを示すフラグです。
  • remember関数は、匿名フィールドとして"error"を持つインターフェース(*ast.InterfaceType)をfixlistというリストに記録します。

reader構造体のcomputeMethodSets関数が実行される際に、もしerrorDecltrue(つまり、ローカルにerror型が宣言されている)であれば、fixlistに記録されたすべてのインターフェースに対してremoveErrorField関数が呼び出されます。removeErrorField関数は、インターフェース内の匿名フィールドで名前が"error"のものを削除します。

このメカニズムにより、以下の挙動が実現されます。

  1. 事前宣言されたerrorインターフェース: パッケージ内でerrorという名前の型がローカルに宣言されていない場合、匿名フィールドとして埋め込まれたerrorインターフェースはエクスポートされたものとして扱われ、ドキュメントに表示されます。これは、Goの標準ライブラリで定義されているerrorインターフェースを指すため、ドキュメントに含めるのが適切です。
  2. ローカルに宣言されたerror: パッケージ内でerrorという名前の型がローカルに宣言されている場合、そのローカルなerror型が事前宣言されたerrorインターフェースをシャドウ(隠蔽)します。この場合、匿名フィールドとして埋め込まれたerrorは、ローカルな(通常はエクスポートされない)型を指すため、ドキュメントからフィルタリングされるべきです。このコミットの変更により、errorDeclフラグとfixlistの仕組みを使って、このケースで適切にフィルタリングが行われるようになります。

テストファイル選択のための-filesフラグ

src/pkg/go/doc/doc_test.goでは、testingパッケージのflag機能を利用して、新しいコマンドラインフラグ-filesが追加されました。

  • var files = flag.String("files", "", "consider only Go test files matching this regular expression") この行により、go testコマンド実行時に-filesフラグで正規表現を指定できるようになります。
  • test関数内で、このfilesフラグの値が空でない場合、指定された正規表現を用いてファイルフィルタリングロジックが適用されます。
  • parser.ParseDir関数に渡されるフィルタ関数が、isGoFile(Goファイルであるかどうかのチェック)に加えて、正規表現に一致するかどうかのチェックも行うように変更されました。これにより、testdataディレクトリ内の特定のテストファイルのみを対象に解析とテストが行われるようになります。

これらの変更は、go/docパッケージの正確性とテストの利便性を向上させるための重要な改善です。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/pkg/go/doc/doc_test.go:

    • regexpパッケージのインポートが追加されました。
    • flag.Stringを使用して、新しいコマンドラインフラグ-filesが定義されました。
    • test関数内で、-filesフラグが指定されている場合に、正規表現に基づいてテストファイルをフィルタリングするロジックが追加されました。parser.ParseDirに渡すフィルタ関数が動的に変更されます。
  2. src/pkg/go/doc/exports.go:

    • removeErrorFieldという新しい関数が追加されました。この関数は、インターフェース型から匿名フィールドで名前が"error"のものを削除します。
    • filterFieldList関数のシグネチャが変更され、ityp *ast.InterfaceTypeという新しい引数が追加されました。
    • filterFieldList内で、匿名フィールドの名前が"error"である場合に、それが事前宣言されたerrorインターフェースである可能性を考慮し、一時的に保持するロジックが追加されました。また、readerrememberメソッドを呼び出して、このインターフェースをfixlistに記録するようになりました。
    • filterType関数内で、ast.InterfaceTypeの場合にfilterFieldListを呼び出す際に、インターフェース型自身を引数として渡すように変更されました。
    • filterSpec関数内で、型宣言の名前が"error"である場合に、readererrorDeclフラグをtrueに設定するロジックが追加されました。
  3. src/pkg/go/doc/reader.go:

    • embeddedSetという新しい型が定義されました。
    • namedType構造体のembeddedフィールドの型がmap[*namedType]boolからembeddedSetに変更されました。
    • reader構造体に、errorDecl bool(ローカルにerror型が宣言されているかを示すフラグ)とfixlist []*ast.InterfaceType(匿名フィールドとして"error"を持つインターフェースのリスト)という新しいフィールドが追加されました。
    • remember関数が追加されました。これは、fixlistにインターフェースを追加します。
    • lookupType関数内で、namedTypeembeddedフィールドを初期化する際にmake(embeddedSet)を使用するように変更されました。
    • collectEmbeddedMethods関数のシグネチャが変更され、visited map[*namedType]boolからvisited embeddedSetに変更されました。
    • computeMethodSets関数内で、errorDecltrueの場合にfixlist内のすべてのインターフェースに対してremoveErrorFieldを呼び出すロジックが追加されました。

コアとなるコードの解説

src/pkg/go/doc/exports.go

// removeErrorField removes anonymous fields named "error" from an interface.
// This is called when "error" has been determined to be a local name,
// not the predeclared type.
func removeErrorField(ityp *ast.InterfaceType) {
	list := ityp.Methods.List // we know that ityp.Methods != nil
	j := 0
	for _, field := range list {
		keepField := true
		if n := len(field.Names); n == 0 {
			// anonymous field
			if fname, _ := baseTypeName(field.Type); fname == "error" {
				keepField = false
			}
		}
		if keepField {
			list[j] = field
			j++
		}
	}
	if j < len(list) {
		ityp.Incomplete = true
	}
	ityp.Methods.List = list[0:j]
}

func (r *reader) filterFieldList(parent *namedType, fields *ast.FieldList, ityp *ast.InterfaceType) (removedFields bool) {
    // ... (既存のコード) ...
    if n := len(field.Names); n == 0 {
        // anonymous field
        fname := r.recordAnonymousField(parent, field.Type)
        if ast.IsExported(fname) {
            keepField = true
        } else if ityp != nil && fname == "error" {
            // possibly the predeclared error interface; keep
            // it for now but remember this interface so that
            // it can be fixed if error is also defined locally
            keepField = true
            r.remember(ityp) // ここでインターフェースを記録
        }
    }
    // ... (既存のコード) ...
}

func (r *reader) filterSpec(spec ast.Spec) bool {
    // ... (既存のコード) ...
    case *ast.TypeSpec:
        if name := s.Name.Name; ast.IsExported(name) {
            r.filterType(r.lookupType(s.Name.Name), s.Type)
            return true
        } else if name == "error" {
            // special case: remember that error is declared locally
            r.errorDecl = true // ここでローカルなerror型宣言を検出
        }
    // ... (既存のコード) ...
}

removeErrorField関数は、インターフェースから匿名で埋め込まれたerrorフィールドを削除する役割を担います。これは、ローカルにerror型が宣言されている場合にのみ呼び出され、事前宣言されたerrorインターフェースとローカルなerror型を区別するために使用されます。 filterFieldList関数は、匿名フィールドが"error"である場合に、それが事前宣言されたerrorインターフェースである可能性を考慮し、一時的にkeepFieldtrueに設定します。そして、r.remember(ityp)を呼び出して、このインターフェースを後で処理するためにfixlistに記録します。 filterSpec関数は、パッケージ内でerrorという名前の型が宣言されている場合(type error interface { ... }のような宣言)、r.errorDeclフラグをtrueに設定します。これにより、go/docはローカルなerror型が存在することを認識します。

src/pkg/go/doc/reader.go

type reader struct {
    // ... (既存のフィールド) ...
    // support for package-local error type declarations
    errorDecl bool                 // if set, type "error" was declared locally
    fixlist   []*ast.InterfaceType // list of interfaces containing anonymous field "error"
}

func (r *reader) remember(typ *ast.InterfaceType) {
    r.fixlist = append(r.fixlist, typ)
}

func (r *reader) computeMethodSets() {
    // ... (既存のコード) ...

    // if error was declared locally, don't treat it as exported field anymore
    if r.errorDecl {
        for _, ityp := range r.fixlist {
            removeErrorField(ityp) // ローカルなerror型が存在する場合にのみ、匿名errorフィールドを削除
        }
    }
}

reader構造体には、errorDeclfixlistという新しいフィールドが追加されました。これらは、ローカルなerror型宣言の有無と、匿名errorフィールドを持つインターフェースのリストを追跡するために使用されます。 remember関数は、filterFieldListから呼び出され、匿名errorフィールドを持つインターフェースをfixlistに追加します。 computeMethodSets関数は、ドキュメント生成プロセスの後半で呼び出されます。ここで、もしr.errorDecltrueであれば(つまり、ローカルにerror型が宣言されている場合)、fixlistに記録されたすべてのインターフェースに対してremoveErrorFieldが呼び出されます。これにより、ローカルなerror型が事前宣言されたerrorインターフェースをシャドウしている場合に、ドキュメントから匿名errorフィールドが適切にフィルタリングされます。

src/pkg/go/doc/doc_test.go

var files = flag.String("files", "", "consider only Go test files matching this regular expression")

func test(t *testing.T, mode Mode) {
	// determine file filter
	filter := isGoFile
	if *files != "" {
		rx, err := regexp.Compile(*files)
		if err != nil {
			t.Fatal(err)
		}
		filter = func(fi os.FileInfo) bool {
			return isGoFile(fi) && rx.MatchString(fi.Name())
		}
	}

	// get packages
	fset := token.NewFileSet()
	pkgs, err := parser.ParseDir(fset, dataDir, filter, parser.ParseComments)
    // ... (既存のコード) ...
}

filesというstring型のフラグが定義され、コマンドラインから正規表現を受け取れるようになりました。 test関数内で、このfilesフラグが空でない場合、指定された正規表現をコンパイルし、filter関数を上書きします。新しいfilter関数は、元のisGoFileのチェックに加えて、ファイル名が正規表現に一致するかどうかもチェックします。 最終的に、parser.ParseDir関数にこの動的に生成されたfilter関数が渡され、テスト対象のファイルを絞り込むことができます。

関連リンク

参考にした情報源リンク