[インデックス 12369] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc
におけるファイルシステム処理の根本的な改善と簡素化を目的としています。特に $GOPATH
のサポートを強化し、内部のファイルシステムコードをPlan 9スタイルの名前空間に統一することで、より堅牢で理解しやすい構造へと変更されています。
コミット
commit fae0d35043b7a3f8f3673d79cbf1d4798ee5e5aa
Author: Russ Cox <rsc@golang.org>
Date: Mon Mar 5 10:02:46 2012 -0500
godoc: support $GOPATH, simplify file system code
The motivation for this CL is to support $GOPATH well.
Since we already have a FileSystem interface, implement a
Plan 9-style name space. Bind each of the $GOPATH src
directories onto the $GOROOT src/pkg directory: now
everything is laid out exactly like a normal $GOROOT and
needs very little special case code.
The filter files are no longer used (by us), so I think they
can just be deleted. Similarly, the Mapping code and the
FileSystem interface were two different ways to accomplish
the same end, so delete the Mapping code.
Within the implementation, since FileSystem is defined to be
slash-separated, use package path consistently, leaving
path/filepath only for manipulating operating system paths.
I kept the -path flag, but I think it can be deleted too.
Fixes #2234.
Fixes #3046.
R=gri, r, r, rsc
CC=golang-dev
https://golang.org/cl/5711058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fae0d35043b7a3f8f3673d79cbf1d4798ee5e5aa
元コミット内容
godoc: support $GOPATH, simplify file system code
この変更の動機は、$GOPATH を適切にサポートすることです。 既存の FileSystem インターフェースがあるため、Plan 9 スタイルの名前空間を実装します。 各 $GOPATH の src ディレクトリを $GOROOT の src/pkg ディレクトリにバインドします。これにより、すべてが通常の $GOROOT とまったく同じように配置され、特別なケースのコードがほとんど不要になります。
フィルターファイルはもはや使用されていないため(我々によって)、削除できると思います。同様に、Mapping コードと FileSystem インターフェースは同じ目的を達成するための2つの異なる方法であったため、Mapping コードを削除します。
実装内では、FileSystem がスラッシュ区切りで定義されているため、一貫してパッケージ path
を使用し、オペレーティングシステムパスの操作には path/filepath
のみを使用します。
-path
フラグは残しましたが、これも削除できると思います。
Fixes #2234. Fixes #3046.
変更の背景
godoc
はGo言語のソースコードからドキュメンテーションを生成し、HTTPサーバーとして提供するツールです。初期の godoc
は $GOROOT
(Goのインストールディレクトリ) 内のパッケージを主に扱っていましたが、Go 1から導入された $GOPATH
(ユーザーのワークスペースディレクトリ) の概念により、ユーザーが独自のパッケージを $GOPATH
内に配置することが一般的になりました。
この変更以前の godoc
は $GOPATH
のパッケージを扱う際に複雑なロジックや特別な処理を必要としていました。特に、$GOROOT
と $GOPATH
の両方からパッケージを検索し、それらを統合して表示する仕組みが非効率的で、重複したコードや概念(Mapping
や httpzip
など)が存在していました。
このコミットは、以下の課題を解決することを目的としています。
$GOPATH
のサポート強化:$GOPATH
にあるパッケージを$GOROOT
のパッケージと同じようにシームレスに扱えるようにする。- ファイルシステムコードの簡素化: 複数のファイルシステム抽象化(
FileSystem
インターフェース、Mapping
、httpzip
)が混在しており、これを統一されたアプローチで整理する。 - パス処理の一貫性: OS固有のパス処理(
path/filepath
)と、Goパッケージパスのような抽象的なスラッシュ区切りパス処理(path
)の使い分けを明確にし、コードの可読性と保守性を向上させる。 - 冗長な機能の削除: 不要になったフィルターファイルや
Mapping
コード、httpzip
実装を削除し、コードベースをスリム化する。
具体的には、Issue #2234 (godoc: support GOPATH
) と Issue #3046 (godoc: -path flag is confusing
) の解決が明示されています。
前提知識の解説
1. godoc
とその役割
godoc
はGo言語の公式ドキュメンテーションツールであり、Goのソースコード(.go
ファイル)からコメントや宣言を解析し、HTML形式のドキュメントを生成します。また、HTTPサーバーとして動作し、ブラウザを通じてこれらのドキュメントを閲覧できるようにします。Goの標準ライブラリのドキュメントも godoc
によって生成されています。
2. $GOROOT
と $GOPATH
$GOROOT
: Go言語のインストールディレクトリを指します。Goの標準ライブラリのソースコード(src/pkg
以下)やツールなどが含まれています。$GOPATH
: Goのワークスペースディレクトリを指します。Go 1から導入された概念で、ユーザーが開発するGoプロジェクトのソースコード、コンパイルされたバイナリ、パッケージなどが配置されます。複数のディレクトリをコロン(Windowsではセミコロン)で区切って指定できます。$GOPATH/src
以下にGoのソースコードが置かれます。
3. Plan 9 スタイルの名前空間
Plan 9 はベル研究所で開発された分散オペレーティングシステムです。その特徴の一つに「すべてがファイルである」という哲学と、柔軟な「名前空間」の概念があります。Plan 9 の名前空間では、異なるファイルシステムやリソースを、あたかも単一の階層的なファイルシステムの一部であるかのように、任意のパスに「マウント」(バインド)することができます。これにより、物理的な配置に関わらず、論理的に統一されたリソースのビューを提供できます。
このコミットでは、このPlan 9の名前空間の考え方を godoc
の内部ファイルシステムに適用しています。つまり、物理的に異なる $GOROOT/src/pkg
と $GOPATH/src
の内容を、godoc
内部では /src/pkg
という単一の仮想パスの下に統合して見せるようにします。
4. path
パッケージと path/filepath
パッケージ
Go言語にはパスを扱うための2つの主要なパッケージがあります。
path
パッケージ: スラッシュ (/
) をパス区切り文字とする、抽象的なパス(URLパスやGoのインポートパスなど)を操作するための関数を提供します。OSに依存しないパス処理に適しています。path/filepath
パッケージ: オペレーティングシステム固有のパス区切り文字(Windowsではバックスラッシュ\
、Unix系ではスラッシュ/
)を使用する、物理ファイルシステムのパスを操作するための関数を提供します。
このコミットでは、godoc
内部の仮想ファイルシステムでは path
パッケージを、実際のOSファイルシステムとのやり取りでのみ path/filepath
を使用するという方針を明確にしています。
5. FileSystem
インターフェース
godoc
は、ファイルシステムへのアクセスを抽象化するために FileSystem
インターフェースを定義していました。これにより、実際のOSファイルシステムだけでなく、ZIPファイル内のコンテンツや、このコミットで導入される仮想ファイルシステムなど、様々なソースからファイルを読み取ることが可能になります。
技術的詳細
このコミットの核心は、src/cmd/godoc/filesystem.go
に導入された nameSpace
型と、それに関連する Bind
メソッドの実装です。
nameSpace
の導入
nameSpace
は map[string][]mountedFS
として定義されており、これはPlan 9スタイルの名前空間を模倣しています。キーはマウントポイント(仮想パス)、値はそのマウントポイントにバインドされた mountedFS
のリストです。
mountedFS
構造体は以下の情報を持っています。
old
: マウント元のパス(例:/src/pkg
)fs
: 実際にファイルを提供するFileSystem
インターフェースの実装(例:osFS
やzipFS
)new
:fs
に渡されるパスのプレフィックス(例:$GOPATH/src
が/src/pkg
にバインドされる場合、new
は/src
となる)
nameSpace
は FileSystem
インターフェースを実装しており、Open
, Lstat
, Stat
, ReadDir
といったファイルシステム操作を、内部でバインドされた複数の FileSystem
実装に委譲します。
$GOPATH
の統合
src/cmd/godoc/main.go
の main
関数内で、godoc
の起動時に nameSpace
が初期化され、$GOROOT
と $GOPATH
のソースディレクトリがこの仮想ファイルシステムにバインドされます。
具体的には、まず $GOROOT
がルート (/
) にバインドされます。
fs.Bind("/", OS(*goroot), "/", bindReplace)
次に、$GOPATH
の各ディレクトリが /src/pkg
にバインドされます。
for _, p := range filepath.SplitList(build.Default.GOPATH) {
fs.Bind("/src/pkg", OS(p), "/src", bindAfter)
}
ここで bindAfter
モードが重要です。これは、既存のバインド(この場合は $GOROOT
の /src/pkg
)の後に新しいバインド($GOPATH
の /src
)を試行することを意味します。これにより、godoc
はまず $GOROOT
内のパッケージを探し、見つからなければ $GOPATH
内のパッケージを探すという優先順位で動作します。結果として、godoc
は /src/pkg
という単一の仮想パスの下で $GOROOT
と $GOPATH
の両方のパッケージを透過的に扱えるようになります。
ReadDir
のロジック
nameSpace
の ReadDir
メソッドは、複数のバインドされたファイルシステムからディレクトリの内容を読み取り、それらを統合する複雑なロジックを持っています。
- Goソースコードを含むディレクトリが見つかった場合、そのディレクトリのファイルはすべて含めます。
- それ以外の場合(Goソースコードを含まないディレクトリ)、サブディレクトリのみを含めます。
- マウントポイントに到達するために必要な中間ディレクトリ(例:
$GOROOT
にsrc/pkg
がなくても、$GOPATH
にsrc
がある場合、src
やpkg
という仮想ディレクトリを作成して表示する)も自動的に生成して表示します。
これにより、ユーザーは $GOROOT
と $GOPATH
の物理的な分離を意識することなく、統一されたパッケージツリーを godoc
上で閲覧できます。
冗長なコードの削除
src/cmd/godoc/mapping.go
の削除: 以前はMapping
型がパスのマッピングを処理していましたが、nameSpace
がその機能をより汎用的に置き換えるため、このファイルは完全に削除されました。src/cmd/godoc/httpzip.go
の削除:http.FileSystem
のZIPファイルベースの実装を提供していましたが、nameSpace
がhttpFS
というラッパーを通じてFileSystem
をhttp.FileSystem
として提供できるようになり、冗長になったため削除されました。- フィルター機能の削除:
-filter
フラグや関連するフィルターファイル処理のコードが削除されました。これは、nameSpace
による統一されたファイルシステムビューが、以前のフィルター機能の必要性を減らしたためと考えられます。
パス処理の一貫性
コミットメッセージにある通り、FileSystem
インターフェースがスラッシュ区切りパスを前提としているため、godoc
内部のパス操作には path
パッケージ(pathpkg
としてインポートされることが多い)が徹底して使用されるようになりました。path/filepath
は、実際のOSファイルシステムとの境界でのみ使用されます。これにより、コードの意図が明確になり、クロスプラットフォームでの動作の信頼性が向上します。
コアとなるコードの変更箇所
このコミットで最も重要な変更は src/cmd/godoc/filesystem.go
ファイルです。
nameSpace
型の定義と実装:type nameSpace map[string][]mountedFS
Open
,Lstat
,Stat
,ReadDir
メソッドの実装が追加され、FileSystem
インターフェースを満たす。Bind
メソッドが追加され、他のFileSystem
を仮想パスにマウントする機能を提供する。resolve
メソッドが、与えられたパスに対してどのmountedFS
を使用すべきかを決定する。
mountedFS
型の定義:type mountedFS struct { old string; fs FileSystem; new string }
translate
メソッドが、仮想パスを実際のFileSystem
が理解できるパスに変換する。
OS
関数の導入:func OS(root string) FileSystem
が追加され、指定されたルートディレクトリを基盤とするOSファイルシステムをFileSystem
インターフェースとして提供する。以前のグローバル変数OS
が関数に置き換えられた。
httpFS
型の導入:type httpFS struct { fs FileSystem }
http.FileSystem
インターフェースを実装し、内部でnameSpace
を利用することで、godoc
のHTTPサーバーが新しい仮想ファイルシステムを透過的に利用できるようにする。
src/cmd/godoc/mapping.go
の削除: パス変換とマッピングのロジックを担っていたこのファイルが完全に削除された。src/cmd/godoc/httpzip.go
の削除: ZIPファイルからHTTPファイルシステムを提供するロジックを担っていたこのファイルが完全に削除された。src/cmd/godoc/main.go
の変更:fs
変数の初期化がnameSpace
を使用するように変更された。$GOPATH
の各ディレクトリがfs.Bind("/src/pkg", OS(p), "/src", bindAfter)
を使って仮想ファイルシステムにバインドされるようになった。fsHttp
の初期化がhttp.FileServer(&httpFS{fs})
を使うように変更された。-filter
フラグやinitDirTrees
の呼び出しが削除された。
src/cmd/godoc/godoc.go
,src/cmd/godoc/dirtrees.go
,src/cmd/godoc/index.go
,src/cmd/godoc/parser.go
,src/cmd/godoc/utils.go
の変更:path/filepath
からpath
パッケージへの移行が広範囲で行われた。Mapping
やフィルター機能に関連するコードが削除または簡素化された。absolutePath
やrelativeURL
といったパス変換ヘルパー関数が削除され、nameSpace
のロジックに置き換えられた。
コアとなるコードの解説
src/cmd/godoc/filesystem.go
の nameSpace
// fs is the file system that godoc reads from and serves.
// It is a virtual file system that operates on slash-separated paths,
// and its root corresponds to the Go distribution root: /src/pkg
// holds the source tree, and so on. This means that the URLs served by
// the godoc server are the same as the paths in the virtual file
// system, which helps keep things simple.
var fs = nameSpace{} // the underlying file system for godoc
// A nameSpace is a file system made up of other file systems
// mounted at specific locations in the name space.
type nameSpace map[string][]mountedFS
// Bind causes references to old to redirect to the path new in newfs.
// If mode is bindReplace, old redirections are discarded.
// If mode is bindBefore, this redirection takes priority over existing ones,
// but earlier ones are still consulted for paths that do not exist in newfs.
// If mode is bindAfter, this redirection happens only after existing ones
// have been tried and failed.
func (ns nameSpace) Bind(old string, newfs FileSystem, new string, mode int) {
// ... (実装詳細) ...
}
// Open implements the FileSystem Open method.
func (ns nameSpace) Open(path string) (readSeekCloser, error) {
var err error
for _, m := range ns.resolve(path) { // パスを解決し、バインドされたFSを順に試す
r, err1 := m.fs.Open(m.translate(path)) // 仮想パスを実際のFSが理解できるパスに変換
if err1 == nil {
return r, nil
}
if err == nil {
err = err1
}
}
// ...
}
// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is.
// (The rest is in resolve.)
// Logically, ReadDir must return the union of all the directories that are named
// by path. In order to avoid misinterpreting Go packages, of all the directories
// that contain Go source code, we only include the files from the first,
// but we include subdirectories from all.
func (ns nameSpace) ReadDir(path string) ([]os.FileInfo, error) {
path = ns.clean(path)
var (
haveGo = false
haveName = map[string]bool{}
all []os.FileInfo
err error
)
for _, m := range ns.resolve(path) { // パスを解決し、バインドされたFSを順に試す
dir, err1 := m.fs.ReadDir(m.translate(path)) // 仮想パスを実際のFSが理解できるパスに変換
if err1 != nil {
// ...
continue
}
// Goファイルが見つかった最初のディレクトリからはファイルもサブディレクトリもすべて含める
// それ以外のディレクトリからはサブディレクトリのみ含める
useFiles := false
if !haveGo {
for _, d := range dir {
if strings.HasSuffix(d.Name(), ".go") {
useFiles = true
haveGo = true
break
}
}
}
for _, d := range dir {
name := d.Name()
if (d.IsDir() || useFiles) && !haveName[name] {
haveName[name] = true
all = append(all, d)
}
}
}
// マウントポイントに到達するために必要な中間ディレクトリを追加
for old := range ns {
if hasPathPrefix(old, path) && old != path {
// ... (中間ディレクトリの追加ロジック) ...
}
}
// ... (ソートとエラーハンドリング) ...
return all, nil
}
nameSpace
は、godoc
が扱うすべてのファイルシステム操作の中心となります。Open
や ReadDir
のようなメソッドは、まず resolve
を呼び出して、与えられた仮想パスに対応する mountedFS
のリストを取得します。その後、リスト内の各 mountedFS
に対して、translate
メソッドで変換されたパスを使って実際のファイルシステム操作を試みます。
ReadDir
のロジックは特に巧妙で、複数の物理的なディレクトリの内容を論理的に統合します。Goソースファイルを含むディレクトリが最初に見つかった場合、そのディレクトリのすべてのファイルとサブディレクトリを含めます。それ以降のディレクトリからは、Goソースファイルが含まれていない限り、サブディレクトリのみを含めることで、パッケージの重複や誤解釈を防ぎます。また、マウントポイントに到達するために必要な仮想的な中間ディレクトリも自動的に生成し、表示します。
src/cmd/godoc/main.go
の main
関数におけるバインド
func main() {
// ... (フラグ解析など) ...
if *zipfile == "" {
// OSのファイルシステムを使用
fs.Bind("/", OS(*goroot), "/", bindReplace)
} else {
// ZIPファイルシステムを使用
rc, err := zip.OpenReader(*zipfile)
// ...
fs.Bind("/", NewZipFS(rc, *zipfile), *goroot, bindReplace)
}
// $GOPATH のツリーをGoルートにバインド
for _, p := range filepath.SplitList(build.Default.GOPATH) {
fs.Bind("/src/pkg", OS(p), "/src", bindAfter)
}
// ... (HTTPハンドラの初期化など) ...
fileServer = http.FileServer(&httpFS{fs}) // 新しいhttpFSラッパーを使用
// ...
}
この部分が、$GOROOT
と $GOPATH
の統合を実現するエントリポイントです。fs.Bind
を使用して、$GOROOT
を仮想ルートに、そして $GOPATH
の各 src
ディレクトリを仮想的な /src/pkg
パスにバインドしています。bindAfter
モードにより、$GOPATH
のパッケージは $GOROOT
のパッケージよりも低い優先順位で検索されます。
関連リンク
- Go言語公式ドキュメンテーション: https://go.dev/doc/
godoc
コマンドのドキュメンテーション: https://go.dev/cmd/godoc/- Go Modules (現代のGoプロジェクト管理): https://go.dev/blog/using-go-modules (このコミットの時点ではGo Modulesは存在しませんが、
$GOPATH
の理解に役立ちます)
参考にした情報源リンク
- Go Issue #2234:
godoc: support GOPATH
- https://github.com/golang/go/issues/2234 - Go Issue #3046:
godoc: -path flag is confusing
- https://github.com/golang/go/issues/3046 - Plan 9 from Bell Labs: https://9p.io/plan9/
- Plan 9 の名前空間に関する解説 (外部記事): https://www.cs.princeton.edu/~bwk/plan9.html (Plan 9の概念を理解するのに役立つ一般的な情報源)
- Go
path
パッケージ: https://pkg.go.dev/path - Go
path/filepath
パッケージ: https://pkg.go.dev/path/filepath