Go filesystems and file embedding

Ben Hoyt

Goのファイルシステムとファイル埋め込み

原文は Ben Hoyt により に公開されました。 このブログを購読する

Goチームは最近、言語、標準ライブラリ、ツールチェーンの変更を提案するいくつかのドラフト設計を公開した。ジェネリクスに関するものについては6月に取り上げた。先週、Goチームはファイルに関連する2つのドラフト設計を公開した。1つはファイルシステム向けの最小限のインターフェースを規定する新しい読み取り専用のファイルシステムインターフェース、もう1つは(ファイルシステムインターフェースを土台として)Goバイナリにファイルを埋め込むための標準的な方法を提案するものだ。Goバイナリへのファイル埋め込みは、プログラムのすべてのリソースを単一のバイナリに含めることでデプロイを簡素化することを目的としており、ファイルシステムインターフェースの設計は主にそのための構成要素として起草された。これらのドラフト設計については多くの議論が交わされ、おおむね好意的に受け止められているが、重大な懸念もいくつか上がっている。

ファイルシステムインターフェースの設計の著者は、GoチームのテックリードであるRuss Coxと、Goの生みの親の一人であるRob Pikeだ。ファイル埋め込みの設計は、Coxと長年Goに貢献してきたBrad Fitzpatrickが著者となっている。さらにCoxは、この形式を好む人向けに、それぞれの設計についてYouTubeでのプレゼンテーション動画も作成した(ファイルシステムインターフェースの動画ファイル埋め込みの動画)。両設計とも、まだ正式な提案ではないことをすぐに明記している。

これはドラフト設計であり、正式なGoの提案ではない。というのも、多くのサードパーティパッケージと同じニーズに対応する潜在的に大規模な変更について述べており、それらの実装に影響を与える可能性があるためだ(うまくいけば簡素化されることを期待して)。このドラフト設計を回覧する目的は、最終的に想定される正式な提案を形作るためのフィードバックを集めることにある。

言語やライブラリの小規模な変更の多くはGitHubのissueトラッカーで議論されているが、こうした大規模な議論については、Goチームは議論をスケールさせるためにr/golangのRedditスレッドを活用しようとしている。GitHubのissueにはスレッド機能がなく、複数の会話を追うのが難しいためだ。各ドラフトにはそれぞれRedditスレッドが用意されている。すなわちファイルシステムインターフェースのスレッドファイル埋め込みのスレッドで、どちらにも多くのコメントが寄せられている。ファイル埋め込みの設計については、長文のHacker Newsスレッドでも議論されている。

ファイルシステムインターフェース

ファイルシステムインターフェース設計の核心は、新しい標準ライブラリパッケージio/fsにおける、FSという名前の単一メソッドのインターフェースである。

    type FS interface {
        Open(name string) (File, error)
    }

これは、すべてのファイルシステム実装が少なくとも名前を指定してファイルを開く機能を実装し、Fileとエラーを返さなければならないことを意味する。Fileインターフェースは次のように定義されている。

    type File interface {
        Stat() (os.FileInfo, error)
        Read(buf []byte) (int, error)
        Close() error
    }

言い換えれば、ファイルは次のような特徴を持つ。すなわち、stat()が返すようなファイル情報を提供でき、読み取りが可能で、クローズできるということだ。これらは準拠するファイルシステムが提供すべき最低限の機能だが、実装は「操作を最適化したり新しい機能を追加したりするために、他のメソッドを提供してもよい」とされている。標準ライブラリのファイル型(os.File)はすでにこれら3つのメソッドを実装しているため、準拠したfs.Fileの実装となっている。

Fileが実際にはディレクトリである場合、Stat()が返すファイル情報がそれを示す。その場合、Open()から返されるFileは、Fileインターフェースに加えてReaddir()メソッドも実装しなければならない。Readdir()はディレクトリ内のファイルを表すos.FileInfoオブジェクトのリストを返す。

ファイルシステムの実装は、設計で「拡張インターフェース」と呼ばれるものを使って追加機能を公開できる。これは「ベースとなるインターフェースを埋め込み、1つ以上の追加メソッドを加えることで、ベースインターフェースのインスタンスが提供しうるオプションの機能を規定するインターフェース」である。例えば、ファイル全体を一度に読み込むことは一般的であり、インメモリのファイルシステム実装では、これをOpen()、複数回のRead()呼び出し、Close()を使って行うのは非効率な場合がある。このような場合、開発者はReadFileFS拡張インターフェースで定義されたReadFile()メソッドを実装できる。

    type ReadFileFS interface {
        FS  // embed the filesystem interface (Open method)
        ReadFile(name string) ([]byte, error)
    }

拡張インターフェースに加えて、設計ではio/fsパッケージにReadFile()ヘルパー関数を追加する。この関数はファイルシステムがReadFileFS拡張を持っているかどうかをチェックし、存在すればそれを使い、なければopen/read/closeの一連の処理にフォールバックする。ドラフト提案では他にも、StatFSReadDirFSGlobFSなど、さまざまな拡張インターフェースが定義されている。設計ではファイルのリネームや書き込みの方法は提供されていないが、それも拡張機能を使って実現可能だ。

新しいio/fsの型やヘルパー関数に加えて、設計ではさまざまな標準ライブラリパッケージを変更して新しいFSインターフェースを活用することが提案されている。例えば、インメモリのファイルシステムからテンプレートをパースできるようにhtml/templateパッケージに追加されるParseFS()メソッドや、開発者がzipファイルをファイルシステムとして扱い、FSが許可されるあらゆる場所で使えるようにarchive/zipパッケージにFS実装させることなどが挙げられている。

Redditでの議論のフィードバックの多くは好意的で、この種のインターフェースは開発者が求めていたもののようだ。しかし、何人かの人々から指摘された批判の1つは、拡張インターフェースの欠点に関するものだ。「Acln0」は懸念を次のように要約した

拡張インターフェースと拡張パターンについて、1つだけ指摘したいことがある。http.ResponseWriterと、httpパッケージが利用しているオプションのインターフェースを思い出す。これらのオプションのインターフェースが存在するせいで、http.ResponseWriterをラップするのは難しくなっている。それを「汎用的に」行おうとするとオプションのインターフェースの組み合わせ爆発を引き起こし、次のような形で簡単に失敗してしまう。「ステータスロギングを追加するためにhttp.ResponseWriterをラップしたところ、ラッパーが下流のハンドラからPushメソッドを隠してしまったため、HTTP/2のpushが動かなくなった」。

著名なGoブロガー兼スピーカーであるPeter Bourgonは、このような拡張インターフェースの使い方は「(極めて有用な)デコレータパターンを使うことが現実的に不可能になる。これは本当に残念だ。私にとっては、この提案はほぼ採用不能になる。デコレータパターンは有用すぎて、このような形で壊してはならない」と考えている。デコレータパターンはインターフェースをラップして機能を追加するものだ。Webサーバのロギングや認証ミドルウェアでよく使われる。ファイルシステムの文脈では、キャッシュや変換レイヤーを追加するために使われる可能性が高い。ミドルウェアの作者がさまざまなオプションのインターフェースを考慮しなければ、結果として得られるラッパーはそれらをサポートしないことになる。Goで書かれたクラウドストレージツールであるRcloneの作者であるNick Craig-Woodは、提案自体は気に入っているものの、同様の懸念を表明した。「拡張(私が普段呼んでいるオプショナル)インターフェースは大きな保守負担になる。ラップするのが本当に難しい」。

設計では「その種のミドルウェアを可能にすることは、このドラフト設計の重要な目標である」と述べられているため、設計の著者がこの問題に正面から取り組むのが賢明だろう。Coxはまだ解決策を提案していないが、この問題を次のように認めた。「確かに、拡張とラッパーの間には緊張関係がある。これに対する完璧な解決策はまだ見つかっていない」。

別の懸念は、「TheSwedeheart」からコンテキスト(Goにおいてタイムアウト、キャンセルシグナル、リクエストスコープの値を呼び出しチェーンに明示的に伝播させる標準的な方法)に関して寄せられた。「[彼の仮想ファイルシステムを]これに移行するために欠けているのは、キャンセルのために各操作へコンテキストを伝播させるサポートだ」。Coxは、ライブラリの作者は「おそらくコンストラクタにコンテキストを渡し、コンテキストを埋め込んだFSを返すようにすれば、その特定のFSで行われる呼び出しにそのコンテキストを適用できるだろう」と返答した。「lobster_johnson」が指摘したように、これはコンテキストを構造体内部に保持せず、コンテキストを最初の関数引数として明示的に渡すというcontextパッケージのガイドラインに反する。しかしCoxは、http.Requestが同様のことをしている例を挙げて次のように反論した。「あれは規則というよりガイドラインだ。 [...] 時にはそれが理にかなうこともある」。

もちろん、命名をめぐるお決まりのバイクシェディングのスレッドもある。「olegkovalov」は次のように述べた。「io/fsという名前に少し不安を感じる。fsは良い変数名なので、io/fsが登場するとユーザーに多くのトラブルを引き起こすだろう」。何度かのやり取りの後、Coxはファイルシステムの実装者ではなくアプリケーション開発者に焦点を当てるために短い名前が必要であることを次のように強調した

あなたはファイルシステムの実装者ではなく、利用者に焦点を当てるべきだ。os.FileInfoos.ModeDiros.PathErroros.ErrNotExistのようなものを参照するコードは、今後はすべて正式にfs.FileInfofs.ModeDirfs.PathErrorfs.ErrNotExistを参照することになる。これらは、例えばfilesystem.ErrNotExistなどよりもはるかに優れている。そして、ファイルシステムを実装するコードよりも、はるかに多くのコードがそれらの名前を参照することになる。

バイナリへのファイル埋め込み

もう1つのドラフト設計は、Goバイナリにファイル(あるいは「静的アセット」)を埋め込み、実行時にその内容を読み取る方法を提案するものだ。これにより、開発者は外部依存のない大きなバイナリを単にコピーするだけで済むため、リリースやデプロイが簡素化される(SQLスニペット、HTMLテンプレート、Webアプリケーション用のCSSやJavaScriptアセットなど)。ドキュメントが指摘するように、これを実現するサードパーティツールはすでに十数種類存在するが、「埋め込みの基本機能についてgoコマンドを直接サポートすることで、これらのツールの一部が不要になり、少なくとも他のツールの実装が簡素化されるだろう」。標準のgoツールに埋め込みを含めることで、ファイルをGoソースコード内のデータに変換するための事前ビルドステップが不要になり、それらの生成ファイルをバージョン管理にコミットする必要もなくなる。

設計の著者たちは、これがGo言語の変更ではなくツールチェーンの変更であることを明確にしている

もう1つの明確な目標は、言語の変更を避けることだ。我々にとって、静的アセットの埋め込みは言語の問題というよりツールチェーンの問題に思える。言語の変更を避けることで、Goコードを処理する多くのツール、例えばgoimportsgoplsstaticcheckなどを更新する必要がなくなる。

goツールはすでに、特定のアーキテクチャでのみ特定のファイルを含めるための// +buildタグや、コード生成のためにどのコマンドを実行するかをgo generateに伝える//go:generateコメントなど、さまざまな目的でGoソースファイル内の特殊なコメントを探している。このファイル埋め込み設計では、変数宣言の直上に置かれ、go buildにそれらのファイルを変数に関連付けて最終的なバイナリに含めるよう指示する新しい//go:embedコメントディレクティブが提案されている。具体的な例を次に示す。

    // The "content" variable holds our static web server content.
    //go:embed image/* template/*
    //go:embed html/index.html
    var content embed.Files

これにより、go buildimageディレクトリとtemplateディレクトリ内のすべてのファイル、ならびにhtml/index.htmlファイルをバイナリに含め、content変数(embed.Files型)を介してアクセス可能にする。embedパッケージは、埋め込まれたファイルにアクセスするためのAPIを含む、提案されている新しい標準ライブラリパッケージだ。さらに、embed.Files型は上記で議論したファイルシステム設計のfs.FSインターフェースを実装しており、埋め込まれたファイルをnet/httphtml/templateなどの他の標準ライブラリパッケージや、新しいファイルシステムインターフェースをサポートするサードパーティパッケージで直接利用できる。

設計は、提案の適用範囲を重要な点で限定している。バイナリに含める前にファイル内のデータを変換する方法は数多く考えられる。データ圧縮、TypeScriptのコンパイル、画像のリサイズなどだ。この設計では、生のファイルデータをそのまま含めるというシンプルなアプローチを取っている。

goコマンドが、望ましい可能性のあるあらゆる変換を予測したり含めたりすることは現実的ではない。goコマンドは汎用的なビルドシステムでもない。特に、ビルド中にユーザープログラムを決して実行しないという設計上の制約を思い出してほしい。こうした種類の変換は、goコマンドが埋め込むべき正確なバイト列を書き出すことができるMakeやBazelのような外部ビルドシステムに任せるのが最適だ。

Redditスレッドでのフィードバックもまたおおむね好意的で、「bojanz」によるこのコメントのようなものがあった。「素晴らしい出発点のようだ。これに取り組んでくれてありがとう」。いくつかの軽微な提案もある。例えば「zikaeroh」による、Pythonにおけるglob('**/*.png', recursive=True)のように、再帰的なパスマッチングのためにダブルスターをサポートするより強力なパスマッチングAPIを追加することに賛成するコメントだ。ファイル埋め込みパッケージのメンテナーであるKevin Burkeは、開発者が実行時にファイルをハッシュする必要がないように、各ファイルの内容の暗号学的ハッシュも保存することを提案した。「これは例えば静的ファイルサーバでのキャッシュバスティングに役立つ」。

繰り返し見られる批判の1つは、特別な//go:embed構文でソースコードのコメントを過剰に利用することを好まない開発者からのものだ。「Saturn_vk」は「コメントが実際の作業のために乱用されているのは本当に好きではない」と率直に述べた。Hacker Newsのコメンターである「breakingcups」は、コメント内のディレクティブの代わりにプロジェクトファイルを使うことを強く主張した

またしてもマジックコメントか。

提案されている機能自体は素晴らしいが、Goチームが別途明確に定義されたプロジェクトファイルや、少なくともコードファイル内での別の構文を使うことを嫌がるため、追加機能のすべてがコメントという人間のメモ書きと共有される空間に詰め込まれることになる。

Coxはこの点についての考えを、Cにおける#pragmaと比較した次のコメントでまとめている。

参考までに、我々はすでに//go:generateや、あまり知られていない他のいくつかのものを持っている。そして// +build//go:buildに置き換える別のドラフト設計もある。その時点で完全に一貫することになる。すなわち、この種のディレクティブは//go:で始まるということだ。ポイントは、知る必要のないツールには無視される程度にコメントらしく見せつつ、人に対しては何か特別なことが起きていることを知らせる程度にはコメントらしくなく見せることにある。

Cではこれに#pragma fooを使っている。Goでは単に#pragma//go:と綴っているだけだ。

今後の展望

両方のドラフト設計、特にユーザーに近いファイル埋め込みの提案については、コミュニティからの支持がかなり集まっている。多くの開発者はすでにデプロイを簡素化するためにサードパーティのファイル埋め込みライブラリを利用しており、今回の取り組みによりそのツールが標準化されることになる。設計が洗練され、正式な提案になる可能性は高い。Go 1.15が8月1日にリリース予定であることを考えると、これらの提案はGo 1.16(6か月後に予定)に向けて準備が整う可能性もあるが、例えば拡張インターフェースの問題に関して、もう一巡のフィードバックが必要な場合は、1年後のGo 1.17に含まれる可能性の方が高い。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント