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

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

このコミットは、Go言語のドキュメント生成ツールである go/doc パッケージにおいて、Go Playgroundで実行可能な「ファイル全体を対象とするExample(例)」の処理方法を根本的に見直すものです。具体的には、ExampleコードがGo Playgroundの要件(package mainmain関数)に適合するように変換するロジックが強化され、未解決の識別子(importが必要なもの)の検出と、Goの組み込み識別子(事前宣言された型、関数、定数)の適切な除外が改善されています。これにより、Goの公式ドキュメントやgodocで表示されるExampleが、Go Playgroundでより正確かつ確実に動作するようになります。

コミット

commit 81ae666f16bb0b747d1dddef0be4f7dd8b285c49
Author: Andrew Gerrand <adg@golang.org>
Date:   Thu Oct 4 07:55:24 2012 +1000

    go/doc: rewrite whole file examples for playground
    
    R=gri
    CC=gobot, golang-dev
    https://golang.org/cl/6592061

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

https://github.com/golang/go/commit/81ae666f16bb0b747d1dddef0be4f7dd8b285c49

元コミット内容

    go/doc: rewrite whole file examples for playground
    
    R=gri
    CC=gobot, golang-dev
    https://golang.org/cl/6592061

変更の背景

Go言語のドキュメントシステムでは、コード例(Example)を記述し、それが実際に動作することを示すことができます。これらのExampleは、go testコマンドによってテストされるだけでなく、godocツールやGo Playgroundといったオンライン環境で実行可能にすることで、ユーザーがコードを試したり理解を深めたりするのに役立ちます。

しかし、Go Playgroundは特定の制約(package mainであること、エントリポイントとしてmain関数を持つことなど)の下で動作します。従来のgo/docパッケージでは、Exampleコードがファイル全体を占める場合(例えば、ExampleFのような関数ではなく、main関数を含む完全なプログラムとして記述された場合)に、Go Playgroundの要件に適切に変換するロジックが不十分でした。特に、Exampleコード内で使用されているが明示的にimportされていないパッケージ(Goの組み込み型や関数など)の扱いが課題となっていました。

このコミットは、これらの「ファイル全体を対象とするExample」をGo Playgroundでより正確に、かつ堅牢に実行できるようにするための改善を目的としています。これにより、ユーザーはドキュメントに記載されたExampleをGo Playgroundでシームレスに試すことができ、学習体験が向上します。

前提知識の解説

このコミットの理解には、以下のGo言語および関連ツールの知識が役立ちます。

  • Go Playground: Goコードをブラウザ上で実行できるオンラインサービス。通常、package mainで始まり、func main()を持つコードを期待します。
  • go/docパッケージ: Goのソースコードからドキュメントを生成するための標準ライブラリ。Exampleコードの解析と整形も担当します。
  • go/astパッケージ: Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するための標準ライブラリ。このコミットでは、ASTを操作してExampleコードを変換しています。
  • ast.File: Goのソースファイル全体を表すASTノード。
  • ast.BlockStmt: コードブロック({ ... })を表すASTノード。関数の本体などがこれに該当します。
  • ast.FuncDecl: 関数宣言を表すASTノード。
  • ast.GenDecl: 一般的な宣言(import、const、type、var)を表すASTノード。
  • ast.ImportSpec: import文の個々のインポートパスを表すASTノード。
  • ast.Ident: 識別子(変数名、関数名、パッケージ名など)を表すASTノード。
  • ast.Inspect: ASTを走査するためのユーティリティ関数。
  • Example関数: Goのドキュメントでコード例を示すために使用される特別な関数。Exampleで始まる関数名を持つ。ExampleHelloのように特定の関数や型のExampleを示すものと、Example単独でファイル全体のExampleを示すものがある。
  • Output:コメント: GoのExampleコードの末尾に記述される特別なコメント。Exampleの実行結果の期待値を指定するために使用されます。go testはこれと実際の出力を比較します。
  • 事前宣言された識別子 (Predeclared Identifiers): Go言語に組み込まれている型(int, string, boolなど)、関数(len, cap, makeなど)、定数(true, false, iotaなど)のこと。これらは明示的なimport文なしで使用できます。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. playExampleFile関数の導入:

    • ファイル全体をExampleとして扱うための新しい関数 playExampleFilesrc/pkg/go/doc/example.go に追加されました。
    • この関数は、入力された*ast.FileをGo Playgroundで実行可能な形式(package mainfunc main()を持つファイル)に変換します。
    • 具体的には、Example関数(ExampleXxx)をmain関数にリネームし、その関数本体からOutput:コメントを削除する処理を行います。
    • ファイルの先頭にある著作権コメント("Copyright"で始まるもの)も削除されます。
  2. 未解決の識別子検出の改善:

    • playExample関数(Exampleの関数本体をGo Playground用に変換する関数)内で、未解決の識別子(つまり、importが必要なパッケージ名)を検出するロジックが改善されました。
    • 以前は[]*ast.Ident(スライス)で管理されていた未解決の識別子が、map[string]bool(マップ)に変更されました。これにより、識別子の重複チェックや削除が効率的になります。
    • ast.Inspectを使用してASTを走査し、SelectorExpr(例: fmt.Printlnfmt)や単独のIdent(例: Printlnfmtパッケージからインポートされている場合)から未解決の識別子を収集します。
  3. 事前宣言された識別子の適切な除外:

    • 未解決の識別子リストから、Go言語に組み込まれている事前宣言された識別子(nilboolなどの型、lenなどの関数、trueなどの定数)を明示的に除外するロジックが追加されました。
    • これは、src/pkg/go/doc/reader.goに新しく定義されたpredeclaredFuncspredeclaredConstantsマップ、および既存のpredeclaredTypesマップを使用して行われます。これらの識別子はimportを必要としないため、Go Playground用のコード生成時に誤ってimportを合成しないようにします。
  4. Output:コメント処理の共通化とリファクタリング:

    • Exampleコードの末尾にあるOutput:コメントを検出・削除し、関数のブロックの終了位置を調整するロジックが、stripOutputCommentという新しいヘルパー関数として抽出されました。
    • また、ブロック内の最後のコメントを見つけるためのlastCommentヘルパー関数も導入され、コードの重複が排除され、可読性が向上しました。
  5. 堅牢性の向上:

    • playExample関数は、すべての未解決の識別子を解決できなかった場合(つまり、必要なimportを特定できなかった場合)にnilを返すようになりました。これにより、Go Playgroundでビルドできない不正なコードが生成されるのを防ぎます。

これらの変更により、go/docパッケージはExampleコードのASTをより正確に分析し、Go Playgroundの実行環境に合わせた適切なASTを合成できるようになりました。

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

src/pkg/go/doc/example.go

  • Examples関数:

    --- a/src/pkg/go/doc/example.go
    +++ b/src/pkg/go/doc/example.go
    @@ -69,6 +69,7 @@ func Examples(files ...*ast.File) []*Example {
     			// other top-level declarations, and no tests or
     			// benchmarks, use the whole file as the example.
     			flist[0].Code = file
    +			flist[0].Play = playExampleFile(file)
     		}
     		list = append(list, flist...)\n
    
    • ファイル全体がExampleとして扱われる場合に、PlayフィールドにplayExampleFileの戻り値を設定するようになりました。
  • playExample関数:

    • 未解決の識別子をmap[string]boolで管理するように変更。
    • 事前宣言された識別子を未解決リストから削除するロジックを追加。
    • stripOutputCommentヘルパー関数を呼び出すように変更。
    • 未解決の識別子が残っている場合にnilを返すように変更。
  • playExampleFile関数 (新規追加):

    func playExampleFile(file *ast.File) *ast.File {
    	// Strip copyright comment if present.
    	comments := file.Comments
    	if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
    		comments = comments[1:]
    	}
    
    	// Copy declaration slice, rewriting the ExampleX function to main.
    	var decls []ast.Decl
    	for _, d := range file.Decls {
    		if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
    			// Copy the FuncDecl, as it may be used elsewhere.
    			newF := *f
    			newF.Name = ast.NewIdent("main")
    			newF.Body, comments = stripOutputComment(f.Body, comments)
    			d = &newF
    		}
    		decls = append(decls, d)
    	}
    
    	// Copy the File, as it may be used elsewhere.
    	f := *file
    	f.Name = ast.NewIdent("main")
    	f.Decls = decls
    	f.Comments = comments
    	return &f
    }
    
    • ファイル全体のExampleをGo Playground用に変換する主要なロジック。ExampleXxx関数をmainにリネームし、Output:コメントを処理します。
  • stripOutputComment関数 (新規追加):

    func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
    	// Do nothing if no "Output:" comment found.
    	i, last := lastComment(body, comments)
    	if last == nil || !outputPrefix.MatchString(last.Text()) {
    		return body, comments
    	}
    
    	// Copy body and comments, as the originals may be used elsewhere.
    	newBody := &ast.BlockStmt{
    		Lbrace: body.Lbrace,
    		List:   body.List,
    		Rbrace: last.Pos(),
    	}
    	newComments := make([]*ast.CommentGroup, len(comments)-1)
    	copy(newComments, comments[:i])
    	copy(newComments[i:], comments[i+1:])
    	return newBody, newComments
    }
    
    • Output:コメントを削除し、ブロックの終了位置を調整する共通ロジック。
  • lastComment関数 (新規追加):

    func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
    	pos, end := b.Pos(), b.End()
    	for j, cg := range c {
    		if cg.Pos() < pos {
    			continue
    		}
    		if cg.End() > end {
    			break
    		}
    		i, last = j, cg
    	}
    	return
    }
    
    • 指定されたブロック内の最後のコメントを見つけるヘルパー関数。

src/pkg/go/doc/reader.go

  • 事前宣言された識別子マップの追加と移動:
    --- a/src/pkg/go/doc/reader.go
    +++ b/src/pkg/go/doc/reader.go
    @@ -515,29 +515,6 @@ func (r *reader) readPackage(pkg *ast.Package, mode Mode) {
     // ----------------------------------------------------------------------------
     // Types
     
    -var predeclaredTypes = map[string]bool{
    -... (既存のpredeclaredTypesが移動)
    -}
    -
     func customizeRecv(f *Func, recvTypeName string, embeddedIsPtr bool, level int) *Func {
     	if f == nil || f.Decl == nil || f.Decl.Recv == nil || len(f.Decl.Recv.List) != 1 {
     		return f // shouldn't happen, but be safe
    @@ -772,3 +749,53 @@ func sortedFuncs(m methodSet, allMethods bool) []*Func {
     	)
     	return list
     }
    +
    +// ----------------------------------------------------------------------------
    +// Predeclared identifiers (minus "nil")
    +
    +var predeclaredTypes = map[string]bool{
    +	"bool":       true,
    +	"byte":       true,
    +	"complex64":  true,
    +	"complex128": true,
    +	"error":      true,
    +	"float32":    true,
    +	"float64":    true,
    +	"int":        true,
    +	"int8":       true,
    +	"int16":      true,
    +	"int32":      true,
    +	"int64":      true,
    +	"rune":       true,
    +	"string":     true,
    +	"uint":       true,
    +	"uint8":      true,
    +	"uint16":     true,
    +	"uint32":     true,
    +	"uint64":     true,
    +	"uintptr":    true,
    +}
    +
    +var predeclaredFuncs = map[string]bool{
    +	"append":  true,
    +	"cap":     true,
    +	"close":   true,
    +	"complex": true,
    +	"copy":    true,
    +	"delete":  true,\n
    +	"imag":    true,
    +	"len":     true,
    +	"make":    true,
    +	"new":     true,
    +	"panic":   true,
    +	"print":   true,
    +	"println": true,
    +	"real":    true,
    +	"recover": true,
    +}
    +
    +var predeclaredConstants = map[string]bool{
    +	"iota":  true,
    +	"true":  true,
    +	"false": true,
    +}
    
    • predeclaredTypesマップがファイルの後半に移動し、predeclaredFuncspredeclaredConstantsという新しいマップが追加されました。これらはGoの組み込み識別子を網羅的にリストアップし、go/docパッケージがExampleコードの依存関係をより正確に判断するために使用されます。

コアとなるコードの解説

このコミットの核心は、Go Playgroundの制約に合わせてExampleコードのASTを動的に書き換える能力の向上にあります。

  1. playExampleFileの役割:

    • go/docは、Exampleコードがファイル全体を構成していると判断した場合、このplayExampleFile関数を呼び出します。
    • この関数は、元のExampleファイル(package foofunc ExampleFoo()などを含むかもしれない)を、Go Playgroundが期待するpackage mainfunc main()を持つ形式に変換します。
    • 特に重要なのは、ExampleXxxという名前の関数をmain関数にリネームする部分です。これにより、Go PlaygroundがExampleコードをエントリポイントとして認識し、実行できるようになります。
    • 著作権コメントの削除は、Go Playgroundに送信されるコードの冗長性を減らすためのものです。
  2. 未解決識別子と事前宣言された識別子の処理:

    • playExample関数は、Exampleコード内で使用されているが、そのAST上ではどのパッケージに属するかが解決されていない識別子(例: Printlnfmtパッケージからインポートされている場合、fmtは解決済みだがPrintlnは未解決)を特定します。
    • これらの未解決識別子の中から、Go言語に組み込まれている事前宣言された識別子(len, make, intなど)を除外します。これは、これらの識別子がimport文を必要としないためです。
    • 残った未解決識別子に基づいて、必要なimport文を動的に合成し、Go Playgroundに送信されるASTに追加します。これにより、ExampleコードがGo Playground上で正しくコンパイル・実行されるために必要な依存関係が自動的に満たされます。
    • 未解決の識別子が残ってしまった場合(つまり、必要なimportを特定できなかった場合)には、nilを返すことで、Go Playgroundでビルドエラーになる可能性のあるコードの生成を避けます。
  3. Output:コメント処理の洗練:

    • Output:コメントは、Exampleのテスト時に期待される出力を指定するためのものです。Go Playgroundで実行する際には、このコメント自体はコードの一部ではないため、削除する必要があります。
    • stripOutputComment関数は、この削除処理を共通化し、ExampleのASTからOutput:コメントを安全に取り除きます。これにより、Go Playgroundに送信されるコードがクリーンになり、実行結果に影響を与えません。

これらの変更は、GoのドキュメントシステムとGo Playgroundの連携を強化し、Go言語のExampleがよりインタラクティブで信頼性の高いものになるように貢献しています。

関連リンク

参考にした情報源リンク