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

[インデックス 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 の結果が実行ごとに異なる可能性がありました。

新しい実装では、以下の手順を踏むことで決定論性を確保しています。

  1. pkg.Files マップからすべてのファイル名(キー)を抽出し、filenames という文字列スライスに格納します。
  2. sort.Strings(filenames) を呼び出して、この filenames スライスを辞書順にソートします。
  3. 以降の処理(パッケージコメントの収集、宣言のマージ、インポートのマージなど)では、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つです。

  1. src/cmd/godoc/godoc.go: godoc コマンドの主要なロジックが含まれるファイル。ここでは、doc.Mode の扱いに関する軽微なクリーンアップが行われました。
  2. 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 ブロック内に移動されました。これにより、docModeshowSource モードでない場合にのみ関連するようになり、コードのスコープが適切になりました。

  • if docMode&doc.AllDecls == 0if 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)
    

関連リンク

参考にした情報源リンク