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

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

このコミットは、Go言語のドキュメンテーションツールであるgodocの挙動を修正するものです。具体的には、単一のディレクトリ内に複数のGoパッケージが存在する場合に、godocがエラーを適切に報告するように変更されています。これまでは、このような状況で不適切な情報が表示される可能性がありましたが、この変更により、明確なエラーメッセージがユーザーに提示されるようになります。

コミット

commit f597fa67c16dbce225ceb8482acd92a5c474fc19
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Aug 9 16:10:46 2012 -0700

    godoc: report error for directories with multiple packages
    
    Fixes #3922.
    
    R=rsc, adg
    CC=golang-dev
    https://golang.org/cl/6453094

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

https://github.com/golang/go/commit/f597fa67c16dbce225ceb8482acd92a5c474fc19

元コミット内容

このコミットの元の内容は、godocがディレクトリ内に複数のパッケージが存在する場合にエラーを報告するように修正することです。これは、GoのIssue #3922を解決するためのものです。

変更の背景

Go言語のパッケージ管理において、通常、一つのディレクトリには一つのGoパッケージが含まれることが推奨されています。しかし、何らかの理由で一つのディレクトリ内に複数のpackage宣言を持つGoファイルが存在する場合があります。従来のgodocは、このような状況に遭遇した際に、どのパッケージのドキュメントを表示すべきかを判断しようと試み、場合によっては誤った情報や不完全な情報を表示してしまう可能性がありました。

この挙動はユーザーにとって混乱を招くものであり、godocが提供する情報の正確性を損なうものでした。そのため、GoのIssue #3922として「godoc: report error for directories with multiple packages」(godoc: 複数のパッケージを含むディレクトリに対してエラーを報告する)が提起されました。このコミットは、その問題に対する直接的な解決策として、曖昧な状況で推測して表示するのではなく、明確なエラーを報告するようにgodocの動作を変更することを目的としています。これにより、ユーザーはディレクトリ構造の問題を認識し、修正できるようになります。

前提知識の解説

  • Goパッケージ: Go言語では、関連するソースファイルの集まりを「パッケージ」と呼びます。各Goファイルはpackageキーワードで始まるパッケージ宣言を持ち、通常、同じディレクトリ内のすべてのGoファイルは同じパッケージに属します。
  • godoc: godocはGo言語の公式ドキュメンテーションツールです。Goのソースコードからコメントや宣言を解析し、HTML形式やプレーンテキスト形式でドキュメントを生成・表示します。開発者がコードのドキュメントを簡単に参照できるようにするために非常に重要なツールです。
  • go/buildパッケージ: Goの標準ライブラリの一部であり、Goのビルドプロセスに関連する情報(Goファイルの解析、パッケージのインポートパスの解決、ビルドタグの処理など)を提供します。godocは、このパッケージを利用してGoソースファイルを解析し、パッケージ情報を取得します。
  • go/astパッケージ: Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現するためのデータ構造と関数を提供します。godocは、このパッケージを使用してGoソースファイルを解析し、コードの構造を理解します。
  • token.FileSet: go/parserパッケージでGoソースファイルを解析する際に使用されるファイルセットです。ソースコード内の位置情報を管理します。
  • ast.Package: go/astパッケージにおける、Goパッケージを表す構造体です。パッケージ名や、そのパッケージに属するファイルごとのAST情報を含みます。
  • build.Context: go/buildパッケージにおけるビルドコンテキストを表す構造体です。Goのビルド環境(GOOS, GOARCH, GOPATHなど)に関する情報を含み、ファイルの読み込みやディレクトリのスキャン方法をカスタマイズできます。

技術的詳細

このコミットの主要な変更点は、src/cmd/godoc/godoc.go内のdocServer.getPageInfo関数のロジックにあります。

  1. PageInfo構造体の変更:

    • PageInfo構造体からPList []stringフィールドが削除されました。これは、ディレクトリ内に複数のパッケージが存在する場合に、それらのパッケージ名をリストアップして表示するという従来の挙動が廃止されたためです。
  2. getPageInfo関数の引数変更:

    • getPageInfo関数のシグネチャが変更され、pkgname string引数が削除されました。これは、特定のパッケージ名を指定してそのパッケージの情報を取得するというアプローチから、ディレクトリ内のパッケージの整合性をチェックするアプローチに変わったことを示しています。
  3. 複数パッケージ検出時のエラー報告:

    • 最も重要な変更は、parseDir関数(ディレクトリ内のGoファイルを解析し、パッケージのASTを返す)が複数のast.Packageを返した場合の処理です。
    • 変更前は、複数のパッケージが見つかった場合、godocは「最適な」パッケージを選択しようと試み、他のパッケージのリスト(PList)も生成していました。この選択ロジックは、パッケージ名がディレクトリ名と一致するか、mainパッケージでないか、といった基準に基づいていました。
    • 変更後は、len(pkgs) > 1(複数のパッケージが見つかった場合)の条件で、直ちにエラーを生成して返します。エラーメッセージは、どのディレクトリが複数のパッケージを含んでいるか、そしてそれらのパッケージ名が何であるかを明確に示します。
      return PageInfo{
          Dirname: abspath,
          Err:     fmt.Errorf("%s contains more than one package: %s", abspath, buf.Bytes()),
      }
      
      ここでbuf.Bytes()は、見つかった複数のパッケージ名をカンマ区切りで連結した文字列です。
  4. HTML/テキストテンプレートの変更:

    • lib/godoc/package.htmllib/godoc/package.txtから、PListに関連する表示ロジック("Other packages"セクション)が削除されました。これは、godocが複数のパッケージをリストアップする機能を廃止したため、それに対応する表示も不要になったためです。

これらの変更により、godocはディレクトリ内のパッケージの曖昧さを許容せず、明確なエラーとしてユーザーに通知するようになりました。

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

src/cmd/godoc/godoc.go

--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -810,7 +810,6 @@ func remoteSearchURL(query string, html bool) string {
 
 type PageInfo struct {
 	Dirname  string         // directory containing the package
-	PList    []string       // list of package names found
 	FSet     *token.FileSet // corresponding file set
 	PAst     *ast.File      // nil if no single AST with package exports
 	PDoc     *doc.Package   // nil if no single package documentation
@@ -876,27 +875,24 @@ func packageExports(fset *token.FileSet, pkg *ast.Package) {
 // directories, PageInfo.Dirs is nil. If a directory read error occurred,\n // PageInfo.Err is set to the respective error but the error is not logged.\n //
-func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoMode) PageInfo {
+func (h *docServer) getPageInfo(abspath, relpath string, mode PageInfoMode) PageInfo {
 	var pkgFiles []string
 
-	// If we're showing the default package, restrict to the ones
+	// Restrict to the package files
 	// that would be used when building the package on this
 	// system.  This makes sure that if there are separate
 	// implementations for, say, Windows vs Unix, we don't
 	// jumble them all together.
-	if pkgname == "" {
-		// Note: Uses current binary's GOOS/GOARCH.
-		// To use different pair, such as if we allowed the user
-		// to choose, set ctxt.GOOS and ctxt.GOARCH before
-		// calling ctxt.ScanDir.
-		ctxt := build.Default
-		ctxt.IsAbsPath = pathpkg.IsAbs
-		ctxt.ReadDir = fsReadDir
-		ctxt.OpenFile = fsOpenFile
-		dir, err := ctxt.ImportDir(abspath, 0)
-		if err == nil {
-			pkgFiles = append(dir.GoFiles, dir.CgoFiles...)
-		}
+	// Note: Uses current binary's GOOS/GOARCH.
+	// To use different pair, such as if we allowed the user
+	// to choose, set ctxt.GOOS and ctxt.GOARCH before
+	// calling ctxt.ScanDir.
+	ctxt := build.Default
+	ctxt.IsAbsPath = pathpkg.IsAbs
+	ctxt.ReadDir = fsReadDir
+	ctxt.OpenFile = fsOpenFile
+	if dir, err := ctxt.ImportDir(abspath, 0); err == nil {
+		pkgFiles = append(dir.GoFiles, dir.CgoFiles...)
 	}
 
 	// filter function to select the desired .go files
@@ -917,15 +913,12 @@ func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoM
 	// get package ASTs
 	fset := token.NewFileSet()\n 	pkgs, err := parseDir(fset, abspath, filter)\n-	if err != nil && pkgs == nil {\n-		// only report directory read errors, ignore parse errors\n-		// (may be able to extract partial package information)\n+	if err != nil {\n 		return PageInfo{Dirname: abspath, Err: err}\n 	}\n 
 	// select package
 	var pkg *ast.Package // selected package
-	var plist []string   // list of other package (names), if any
 	if len(pkgs) == 1 {\n 		// Exactly one package - select it.\n 		for _, p := range pkgs {\n@@ -933,49 +926,18 @@ func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoM
 		}\n 
 	} else if len(pkgs) > 1 {\n-		// Multiple packages - select the best matching package: The
-		// 1st choice is the package with pkgname, the 2nd choice is
-		// the package with dirname, and the 3rd choice is a package
-		// that is not called "main" if there is exactly one such
-		// package. Otherwise, don't select a package.
-		dirpath, dirname := pathpkg.Split(abspath)
-
-		// If the dirname is "go" we might be in a sub-directory for
-		// .go files - use the outer directory name instead for better
-		// results.
-		if dirname == "go" {
-			_, dirname = pathpkg.Split(pathpkg.Clean(dirpath))
-		}
-
-		var choice3 *ast.Package
-	loop:
+		// More than one package - report an error.
+		var buf bytes.Buffer
 		for _, p := range pkgs {\n-			switch {\n-			case p.Name == pkgname:\n-				pkg = p\n-				break loop // 1st choice; we are done
-			case p.Name == dirname:\n-				pkg = p // 2nd choice
-			case p.Name != "main":\n-				choice3 = p
+			if buf.Len() > 0 {
+				fmt.Fprintf(&buf, ", ")
 			}\n+			fmt.Fprintf(&buf, p.Name)
 		}\n-		if pkg == nil && len(pkgs) == 2 {\n-			pkg = choice3
-		}\n-
-		// Compute the list of other packages
-		// (excluding the selected package, if any).
-		// (excluding the selected package, if any).
-		plist = make([]string, len(pkgs))
-		i := 0
-		for name := range pkgs {\n-			if pkg == nil || name != pkg.Name {\n-				plist[i] = name
-				i++
-			}\n+		return PageInfo{\n+			Dirname: abspath,\n+			Err:     fmt.Errorf("%s contains more than one package: %s", abspath, buf.Bytes()),
+		}
-		plist = plist[0:i]
-		sort.Strings(plist)
 	}\n 
 	// get examples from *_test.go files
@@ -1041,7 +1003,6 @@ func (h *docServer) getPageInfo(abspath, relpath, pkgname string, mode PageInfoM
 
 	return PageInfo{\n 		Dirname:  abspath,\n-		PList:    plist,\n 		FSet:     fset,\n 		PAst:     past,\n 		PDoc:     pdoc,\n@@ -1065,7 +1026,7 @@ func (h *docServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n 	if relpath == builtinPkgPath {\n 		mode = noFiltering\n 	}\n-	info := h.getPageInfo(abspath, relpath, r.FormValue("p"), mode)\n+	info := h.getPageInfo(abspath, relpath, mode)\n 	if info.Err != nil {\n 		log.Print(info.Err)\n 		serveError(w, r, relpath, info.Err)\n```

### `src/cmd/godoc/main.go`

`main.go`では、`pkgHandler.getPageInfo`と`cmdHandler.getPageInfo`の呼び出しから、`pkgname`引数(空文字列`""`)が削除されています。これは、`getPageInfo`関数のシグネチャ変更に伴うものです。

```diff
--- a/src/cmd/godoc/main.go
+++ b/src/cmd/godoc/main.go
@@ -373,13 +373,11 @@ func main() {
 		}\n 		mode |= showSource\n 	}\n-	// TODO(gri): Provide a mechanism (flag?) to select a package
-	//            if there are multiple packages in a directory.
 
 	// first, try as package unless forced as command
 	var info PageInfo\n 	if !forceCmd {\n-		info = pkgHandler.getPageInfo(abspath, relpath, "", mode)\n+		info = pkgHandler.getPageInfo(abspath, relpath, mode)\n 	}\n 
 	// second, try as command unless the path is absolute
 	var cinfo PageInfo\n 	if !filepath.IsAbs(path) {\n 		abspath = pathpkg.Join(cmdHandler.fsRoot, path)\n-		cinfo = cmdHandler.getPageInfo(abspath, relpath, "", mode)\n+cinfo = cmdHandler.getPageInfo(abspath, relpath, mode)\n 	}\n 
 	// determine what to use

lib/godoc/package.html

PListに関連するHTML要素が削除されています。

--- a/lib/godoc/package.html
+++ b/lib/godoc/package.html
@@ -21,9 +21,6 @@
 			{{if $.Examples}}
 				<dd><a href="#pkg-examples">Examples</a></dd>
 			{{end}}
-			{{if $.PList}}
-				<dd><a href="#pkg-other-packages">Other packages</a></dd>
-			{{end}}
 			{{if $.Dirs}}
 				<dd><a href="#pkg-subdirectories">Subdirectories</a></dd>
 			{{end}}
@@ -167,16 +164,6 @@
 	<pre>{{node_html . $.FSet}}</pre>
 {{end}}
 
-{{with .PList}}
-	<h2 id="pkg-other-packages">Other packages</h2>
-	<p>
-	{{/* PList entries are strings - no need for FSet */}}
-	{{range .}}
-	<a href="?p={{urlquery .}}">{{html .}}</a><br />
-	{{end}}
-	</p>
-{{end}}
-
 {{with .Dirs}}
 	{{/* DirList entries are numbers and strings - no need for FSet */}}
 	{{if $.PDoc}}

lib/godoc/package.txt

PListに関連するテキスト要素が削除されています。

--- a/lib/godoc/package.txt
+++ b/lib/godoc/package.txt
@@ -65,15 +65,6 @@ BUGS
 
 ---------------------------------------
 
-*/}}{{with .PList}}\n-OTHER PACKAGES\n-\n-{{range .}}\n-{{.}}{{end}}\n-{{end}}{{/*\n-\n----------------------------------------\n-\n */}}{{with .Dirs}}\n SUBDIRECTORIES\n {{if $.DirFlat}}{{range .List}}{{if .HasPkg}}\n```

## コアとなるコードの解説

このコミットの核心は、`src/cmd/godoc/godoc.go`内の`docServer.getPageInfo`関数における、ディレクトリ内のGoパッケージの解析と処理ロジックの変更です。

変更前は、`getPageInfo`関数は`parseDir`関数によってディレクトリ内のすべてのGoファイルを解析し、見つかったすべてのパッケージのAST(`pkgs`マップ)を取得していました。もし`pkgs`の要素数が1より大きい場合(つまり、複数のパッケージが見つかった場合)、`godoc`は以下の複雑なロジックで「最適な」パッケージを選択しようと試みていました。

1.  リクエストされたパッケージ名(`pkgname`引数で渡される)と一致するパッケージを最優先。
2.  ディレクトリ名と一致するパッケージ。
3.  `main`パッケージではない、唯一のパッケージ。

この選択ロジックは、ユーザーが意図しないパッケージのドキュメントが表示されたり、混乱を招く可能性がありました。また、選択されなかった他のパッケージは`PList`として保持され、HTML/テキスト出力で「Other packages」として表示されていました。

このコミットでは、この複雑な選択ロジックが完全に削除されました。代わりに、`len(pkgs) > 1`という条件が満たされた場合、つまりディレクトリ内に複数のパッケージが検出された場合、`godoc`は直ちに`fmt.Errorf`を使用してエラーを生成し、そのエラーを`PageInfo.Err`フィールドに設定して返します。エラーメッセージには、問題のディレクトリのパスと、検出されたすべてのパッケージ名が明示的に含まれます。

これにより、`godoc`は曖昧な状況で推測するのではなく、明確なエラーを報告するようになり、ユーザーはディレクトリ構造の問題を迅速に特定し、修正できるようになります。これは、Goの「明示的であること」という設計哲学にも合致する変更と言えます。

また、`PageInfo`構造体から`PList`フィールドが削除され、関連するHTML/テキストテンプレートの表示ロジックも削除されたことで、コードベースが簡素化され、`godoc`の挙動がより予測可能になりました。

## 関連リンク

*   Go Issue #3922: [https://github.com/golang/go/issues/3922](https://github.com/golang/go/issues/3922) (このコミットが修正したIssue)
*   Go CL 6453094: [https://golang.org/cl/6453094](https://golang.org/cl/6453094) (このコミットに対応するGoの変更リスト)

## 参考にした情報源リンク

*   Go Issue Tracker: [https://github.com/golang/go/issues](https://github.com/golang/go/issues)
*   Go Documentation: [https://pkg.go.dev/](https://pkg.go.dev/)
*   `go/build`パッケージ: [https://pkg.go.dev/go/build](https://pkg.go.dev/go/build)
*   `go/ast`パッケージ: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
*   `go/token`パッケージ: [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
*   `godoc`コマンドの公式ドキュメント (Goのバージョンによって内容が異なる場合があります): `go doc cmd/godoc`