[インデックス 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/ (マップの順序に関する一般的な説明)