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

[インデックス 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 の両方からパッケージを検索し、それらを統合して表示する仕組みが非効率的で、重複したコードや概念(Mappinghttpzip など)が存在していました。

このコミットは、以下の課題を解決することを目的としています。

  1. $GOPATH のサポート強化: $GOPATH にあるパッケージを $GOROOT のパッケージと同じようにシームレスに扱えるようにする。
  2. ファイルシステムコードの簡素化: 複数のファイルシステム抽象化(FileSystem インターフェース、Mappinghttpzip)が混在しており、これを統一されたアプローチで整理する。
  3. パス処理の一貫性: OS固有のパス処理(path/filepath)と、Goパッケージパスのような抽象的なスラッシュ区切りパス処理(path)の使い分けを明確にし、コードの可読性と保守性を向上させる。
  4. 冗長な機能の削除: 不要になったフィルターファイルや 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 の導入

nameSpacemap[string][]mountedFS として定義されており、これはPlan 9スタイルの名前空間を模倣しています。キーはマウントポイント(仮想パス)、値はそのマウントポイントにバインドされた mountedFS のリストです。

mountedFS 構造体は以下の情報を持っています。

  • old: マウント元のパス(例: /src/pkg
  • fs: 実際にファイルを提供する FileSystem インターフェースの実装(例: osFSzipFS
  • new: fs に渡されるパスのプレフィックス(例: $GOPATH/src/src/pkg にバインドされる場合、new/src となる)

nameSpaceFileSystem インターフェースを実装しており、Open, Lstat, Stat, ReadDir といったファイルシステム操作を、内部でバインドされた複数の FileSystem 実装に委譲します。

$GOPATH の統合

src/cmd/godoc/main.gomain 関数内で、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 のロジック

nameSpaceReadDir メソッドは、複数のバインドされたファイルシステムからディレクトリの内容を読み取り、それらを統合する複雑なロジックを持っています。

  • Goソースコードを含むディレクトリが見つかった場合、そのディレクトリのファイルはすべて含めます。
  • それ以外の場合(Goソースコードを含まないディレクトリ)、サブディレクトリのみを含めます。
  • マウントポイントに到達するために必要な中間ディレクトリ(例: $GOROOTsrc/pkg がなくても、$GOPATHsrc がある場合、srcpkg という仮想ディレクトリを作成して表示する)も自動的に生成して表示します。

これにより、ユーザーは $GOROOT$GOPATH の物理的な分離を意識することなく、統一されたパッケージツリーを godoc 上で閲覧できます。

冗長なコードの削除

  • src/cmd/godoc/mapping.go の削除: 以前は Mapping 型がパスのマッピングを処理していましたが、nameSpace がその機能をより汎用的に置き換えるため、このファイルは完全に削除されました。
  • src/cmd/godoc/httpzip.go の削除: http.FileSystem のZIPファイルベースの実装を提供していましたが、nameSpacehttpFS というラッパーを通じて FileSystemhttp.FileSystem として提供できるようになり、冗長になったため削除されました。
  • フィルター機能の削除: -filter フラグや関連するフィルターファイル処理のコードが削除されました。これは、nameSpace による統一されたファイルシステムビューが、以前のフィルター機能の必要性を減らしたためと考えられます。

パス処理の一貫性

コミットメッセージにある通り、FileSystem インターフェースがスラッシュ区切りパスを前提としているため、godoc 内部のパス操作には path パッケージ(pathpkg としてインポートされることが多い)が徹底して使用されるようになりました。path/filepath は、実際のOSファイルシステムとの境界でのみ使用されます。これにより、コードの意図が明確になり、クロスプラットフォームでの動作の信頼性が向上します。

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

このコミットで最も重要な変更は src/cmd/godoc/filesystem.go ファイルです。

  1. nameSpace 型の定義と実装:
    • type nameSpace map[string][]mountedFS
    • Open, Lstat, Stat, ReadDir メソッドの実装が追加され、FileSystem インターフェースを満たす。
    • Bind メソッドが追加され、他の FileSystem を仮想パスにマウントする機能を提供する。
    • resolve メソッドが、与えられたパスに対してどの mountedFS を使用すべきかを決定する。
  2. mountedFS 型の定義:
    • type mountedFS struct { old string; fs FileSystem; new string }
    • translate メソッドが、仮想パスを実際の FileSystem が理解できるパスに変換する。
  3. OS 関数の導入:
    • func OS(root string) FileSystem が追加され、指定されたルートディレクトリを基盤とするOSファイルシステムを FileSystem インターフェースとして提供する。以前のグローバル変数 OS が関数に置き換えられた。
  4. httpFS 型の導入:
    • type httpFS struct { fs FileSystem }
    • http.FileSystem インターフェースを実装し、内部で nameSpace を利用することで、godoc のHTTPサーバーが新しい仮想ファイルシステムを透過的に利用できるようにする。
  5. src/cmd/godoc/mapping.go の削除: パス変換とマッピングのロジックを担っていたこのファイルが完全に削除された。
  6. src/cmd/godoc/httpzip.go の削除: ZIPファイルからHTTPファイルシステムを提供するロジックを担っていたこのファイルが完全に削除された。
  7. src/cmd/godoc/main.go の変更:
    • fs 変数の初期化が nameSpace を使用するように変更された。
    • $GOPATH の各ディレクトリが fs.Bind("/src/pkg", OS(p), "/src", bindAfter) を使って仮想ファイルシステムにバインドされるようになった。
    • fsHttp の初期化が http.FileServer(&httpFS{fs}) を使うように変更された。
    • -filter フラグや initDirTrees の呼び出しが削除された。
  8. 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 やフィルター機能に関連するコードが削除または簡素化された。
    • absolutePathrelativeURL といったパス変換ヘルパー関数が削除され、nameSpace のロジックに置き換えられた。

コアとなるコードの解説

src/cmd/godoc/filesystem.gonameSpace

// 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 が扱うすべてのファイルシステム操作の中心となります。OpenReadDir のようなメソッドは、まず resolve を呼び出して、与えられた仮想パスに対応する mountedFS のリストを取得します。その後、リスト内の各 mountedFS に対して、translate メソッドで変換されたパスを使って実際のファイルシステム操作を試みます。

ReadDir のロジックは特に巧妙で、複数の物理的なディレクトリの内容を論理的に統合します。Goソースファイルを含むディレクトリが最初に見つかった場合、そのディレクトリのすべてのファイルとサブディレクトリを含めます。それ以降のディレクトリからは、Goソースファイルが含まれていない限り、サブディレクトリのみを含めることで、パッケージの重複や誤解釈を防ぎます。また、マウントポイントに到達するために必要な仮想的な中間ディレクトリも自動的に生成し、表示します。

src/cmd/godoc/main.gomain 関数におけるバインド

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 のパッケージよりも低い優先順位で検索されます。

関連リンク

参考にした情報源リンク