[インデックス 11152] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc
において、?m=src
モード(ソースコード表示モード)での出力の決定論性を向上させるための変更です。具体的には、go/ast
パッケージの MergePackageFiles
関数が、パッケージ内のファイルをマージする際に、マップのイテレーション順序に依存するのではなく、ファイル名をソートした順序で処理するように修正されました。これにより、godoc
が同じパッケージのソースコードを表示する際に、常に一貫した出力が得られるようになります。また、godoc.go
内の軽微なクリーンアップも含まれています。
コミット
- コミットハッシュ:
c7cdce13f55070383efd8251bce6e95118c32bb2
- 作者: Robert Griesemer gri@golang.org
- 日付: Fri Jan 13 09:32:35 2012 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c7cdce13f55070383efd8251bce6e95118c32bb2
元コミット内容
godoc: make ?m=src mode deterministic
Merge package files in the go/ast MergePackageFiles
function always in the same order (sorted by filename)
instead of map iteration order to obtain the same
package file each time. This functionality is used
by godoc when displaying packages in ?m=src mode.
Also: minor cleanup in godoc.go.
R=rsc
CC=golang-dev
https://golang.org/cl/5540054
変更の背景
Go言語の godoc
ツールは、Goのソースコードからドキュメンテーションを生成し、Webブラウザで表示する機能を提供します。?m=src
モードは、特定のパッケージのソースコード全体を表示するために使用されます。
Go言語のマップ(map
)は、そのイテレーション順序が保証されていません。これは、マップがハッシュテーブルとして実装されており、要素の追加や削除、Goのバージョン、実行環境などによってイテレーション順序が変わりうるためです。
godoc
がパッケージのソースコードを表示する際、複数のファイルにまたがるパッケージの場合、go/ast
パッケージの MergePackageFiles
関数を使用して、それらのファイルを抽象構文木(AST)レベルでマージしていました。もしこのマージ処理がマップのイテレーション順序に依存していた場合、同じパッケージであっても、godoc
を実行するたびにソースコードの表示順序が異なってしまう可能性がありました。これは、ユーザーにとって混乱を招き、テストの再現性にも影響を与える非決定論的な挙動となります。
このコミットは、このような非決定論的な挙動を排除し、godoc ?m=src
モードでの出力が常に一貫したものとなるようにするために行われました。
前提知識の解説
godoc
: Go言語の公式ドキュメンテーションツールです。Goのソースコードからコメントや宣言を抽出し、HTML形式で表示します。ローカルでドキュメンテーションサーバーを起動したり、コマンドラインで特定のパッケージのドキュメントを表示したりできます。?m=src
は、Webインターフェースでソースコードを表示するためのクエリパラメータです。go/ast
パッケージ: Go言語の抽象構文木(Abstract Syntax Tree, AST)を扱うための標準ライブラリパッケージです。Goのソースコードを解析し、その構造をASTとして表現します。コンパイラ、リンター、コードフォーマッター、ドキュメンテーションツールなど、Goのコードをプログラム的に操作する多くのツールで利用されます。go/ast.Package
構造体:go/ast
パッケージ内で、Goのパッケージ全体を表す構造体です。この構造体は、パッケージに属する複数のGoソースファイル(それぞれが*ast.File
型)をマップとして保持しています。マップのキーはファイル名、値は対応する*ast.File
です。go/ast.MergePackageFiles
関数:go/ast
パッケージ内の関数で、複数の*ast.File
を含む*ast.Package
から、それらをマージした単一の*ast.File
を生成します。このマージ処理は、パッケージレベルのコメント、宣言、インポートなどを統合するために使用されます。- Go言語のマップのイテレーション順序: Go言語の
map
型は、キーと値のペアを格納するための組み込みのデータ構造です。Goの仕様では、マップのイテレーション(for range
ループなど)の順序は保証されていません。これは意図的な設計であり、実装の詳細に依存するため、同じマップに対して複数回イテレーションを行っても、異なる順序で要素が返される可能性があります。この非決定論的な挙動は、プログラムの出力がマップのイテレーション順序に依存する場合に問題となることがあります。
技術的詳細
この変更の核心は、go/ast.MergePackageFiles
関数がパッケージ内のファイルを処理する順序を、非決定論的なマップのイテレーション順序から、決定論的なファイル名のソート順に変更した点にあります。
元の実装では、pkg.Files
(これは map[string]*File
型) を直接イテレーションしていました。
for _, f := range pkg.Files {
// ...
}
この for range
ループはマップのイテレーション順序に依存するため、MergePackageFiles
の結果が実行ごとに異なる可能性がありました。
新しい実装では、以下の手順を踏むことで決定論性を確保しています。
pkg.Files
マップからすべてのファイル名(キー)を抽出し、filenames
という文字列スライスに格納します。sort.Strings(filenames)
を呼び出して、このfilenames
スライスを辞書順にソートします。- 以降の処理(パッケージコメントの収集、宣言のマージ、インポートのマージなど)では、
pkg.Files
マップを直接イテレーションする代わりに、ソートされたfilenames
スライスをイテレーションし、そのファイル名を使ってpkg.Files
から対応する*ast.File
を取得します。
filenames := make([]string, len(pkg.Files))
i := 0
for filename, f := range pkg.Files {
filenames[i] = filename
i++
}
sort.Strings(filenames)
// ...
for _, filename := range filenames {
f := pkg.Files[filename]
// ...
}
これにより、MergePackageFiles
が常に同じ順序でファイルを処理するため、生成されるAST、ひいては godoc
の ?m=src
モードでの出力が常に一貫したものになります。
また、src/cmd/godoc/godoc.go
では、docMode
変数の宣言と初期化が、より適切なスコープに移動され、冗長なコードが削除されています。これは機能的な変更ではなく、コードのクリーンアップと可読性の向上を目的としています。
コアとなるコードの変更箇所
このコミットによって変更されたファイルは以下の2つです。
src/cmd/godoc/godoc.go
:godoc
コマンドの主要なロジックが含まれるファイル。ここでは、doc.Mode
の扱いに関する軽微なクリーンアップが行われました。src/pkg/go/ast/filter.go
:go/ast
パッケージの一部で、ASTのフィルタリングやマージに関する機能を提供します。このファイルでMergePackageFiles
関数のロジックが変更され、ファイルのマージ順序が決定論的になりました。
コアとなるコードの解説
src/pkg/go/ast/filter.go
の変更
-
import "sort"
の追加: ファイル名をソートするためにsort
パッケージがインポートされました。 -
MergePackageFiles
関数の変更:- 元の
for _, f := range pkg.Files
ループの前に、filenames
スライスを作成し、pkg.Files
のキー(ファイル名)をすべて収集します。 sort.Strings(filenames)
を呼び出して、このfilenames
スライスをソートします。- パッケージコメントの収集、宣言のマージ、インポートのマージを行う各ループで、
for _, filename := range filenames
を使用し、ソートされたファイル名に基づいてpkg.Files[filename]
から*ast.File
を取得するように変更されました。
--- a/src/pkg/go/ast/filter.go +++ b/src/pkg/go/ast/filter.go @@ -4,7 +4,10 @@ package ast -import "go/token" +import ( + "go/token" + "sort" +) // ---------------------------------------------------------------------------- // Export filtering @@ -291,29 +294,35 @@ var separator = &Comment{noPos, "//"} // func MergePackageFiles(pkg *Package, mode MergeMode) *File { // Count the number of package docs, comments and declarations across - // all package files. + // all package files. Also, compute sorted list of filenames, so that + // subsequent iterations can always iterate in the same order. ndocs := 0 ncomments := 0 ndecls := 0 - for _, f := range pkg.Files { + filenames := make([]string, len(pkg.Files)) + i := 0 + for filename, f := range pkg.Files { + filenames[i] = filename + i++ if f.Doc != nil { ndocs += len(f.Doc.List) + 1 // +1 for separator } ncomments += len(f.Comments) ndecls += len(f.Decls) } + sort.Strings(filenames) // Collect package comments from all package files into a single - // CommentGroup - the collected package documentation. The order - // is unspecified. In general there should be only one file with - // a package comment; but it's better to collect extra comments - // than drop them on the floor. + // CommentGroup - the collected package documentation. In general + // there should be only one file with a package comment; but it's + // better to collect extra comments than drop them on the floor. var doc *CommentGroup var pos token.Pos if ndocs > 0 { list := make([]*Comment, ndocs-1) // -1: no separator before first group i := 0 - for _, f := range pkg.Files { + for _, filename := range filenames { + f := pkg.Files[filename] if f.Doc != nil { if i > 0 { // not the first group - add separator @@ -342,7 +351,8 @@ func MergePackageFiles(pkg *Package, mode MergeMode) *File { funcs := make(map[string]int) // map of global function name -> decls index i := 0 // current index n := 0 // number of filtered entries - for _, f := range pkg.Files { + for _, filename := range filenames { + f := pkg.Files[filename] for _, d := range f.Decls { if mode&FilterFuncDuplicates != 0 { // A language entity may be declared multiple @@ -398,7 +408,8 @@ func MergePackageFiles(pkg *Package, mode MergeMode) *File {\ var imports []*ImportSpec if mode&FilterImportDuplicates != 0 { seen := make(map[string]bool) - for _, f := range pkg.Files { + for _, filename := range filenames { + f := pkg.Files[filename] for _, imp := range f.Imports { if path := imp.Path.Value; !seen[path] { // TODO: consider handling cases where:
- 元の
src/cmd/godoc/godoc.go
の変更
-
docMode
変数の宣言と初期化が、if mode&showSource == 0
ブロック内に移動されました。これにより、docMode
がshowSource
モードでない場合にのみ関連するようになり、コードのスコープが適切になりました。 -
if docMode&doc.AllDecls == 0
がif mode&noFiltering == 0
に変更されました。これは、docMode
の代わりに直接mode
フラグを使用することで、より明確で直接的な条件チェックになります。--- a/src/cmd/godoc/godoc.go +++ b/src/cmd/godoc/godoc.go @@ -1086,18 +1086,18 @@ func (h *httpHandler) getPageInfo(abspath, relpath, pkgname string, mode PageInf var past *ast.File var pdoc *doc.Package if pkg != nil { - var docMode doc.Mode - if mode&noFiltering != 0 { - docMode = doc.AllDecls - } if mode&showSource == 0 { // show extracted documentation - pdoc = doc.New(pkg, path.Clean(relpath), docMode) // no trailing '/' in importpath + var m doc.Mode + if mode&noFiltering != 0 { + m = doc.AllDecls + } + pdoc = doc.New(pkg, path.Clean(relpath), m) // no trailing '/' in importpath } else { // show source code // TODO(gri) Consider eliminating export filtering in this mode, // or perhaps eliminating the mode altogether. - if docMode&doc.AllDecls == 0 { + if mode&noFiltering == 0 { ast.PackageExports(pkg) } past = ast.MergePackageFiles(pkg, ast.FilterUnassociatedComments)
関連リンク
- Go CL (Change List) 5540054: https://golang.org/cl/5540054
参考にした情報源リンク
- Go言語の公式ドキュメンテーション: https://golang.org/doc/
go/ast
パッケージのドキュメンテーション: https://pkg.go.dev/go/astgodoc
コマンドのドキュメンテーション: https://pkg.go.dev/cmd/godoc- Go言語のマップのイテレーション順序に関する情報 (例: Go言語の仕様やブログ記事など)
- The Go Programming Language Specification - Map types: https://go.dev/ref/spec#Map_types
- Go Slices, Maps, and Structs in Depth: https://yourbasic.org/golang/maps-explained/ (マップの順序に関する一般的な説明)