Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

Goのファイルシステムfs.FSを実装するときのヒント

pkg.go.dev

fs.FSを実装するには次のインターフェースを満たす具象型を実装しなければならない。

多いね。それぞれのメソッドとインターフェースの関係は次の図で表すことができる。 例えば、FSのメソッドOpenはFileを返す。

ファイルパスのルール

FSのファイルパスのルールはfsパッケージの関数ValidPathに書いてある。 ざっと次のとおり。

  • UTF-8エンコード
  • パスの区切りは/(例えばpath/to/file
  • パスの前後に/をつけてはいけない(/aaa, aaa/は許されない)
  • パスの要素に(空文字列)や...は許されない(path//to/./dir/../or/fileは許されない)
  • ただしルートは単体の.で示す。

これらはFSを実装しやすくするためのルールだ。 Openメソッドを実装するときには入力されたファイルパスが上のルールに従っているかVaildPathを使ってチェックすると良いだろう。

func (fsys *myfs) Open(name string) (fs.File, error) {
    if !fs.ValidPath(name) {
        return nil, &fs.PathError{"open", name, fs.ErrInvalid}
    }
    ...
}

fs.ReadDirFileとfs.ReadDirFSの違い

ReadDirFileはFileにReadDirメソッドが追加されたインターフェース。 ReadDirFSはFSにReadDirメソッドが追加されたインターフェースである。 実装したファイルシステムにどちらかのインターフェースが備わっていればディレクトリの中身を読むことができる。 しかしながら好きな方どっちかを実装すればよいという訳ではない。 Goのドキュメントでは全てのディレクトリファイルはReadDirFileとして実装するべきと書いてある。 ReadDirFSはfsパッケージの関数ReadDir(上のメソッドとは区別せよ)を最適化するための補助的なインターフェースであって、ReadDirFSだからReadDirFileを実装しなくていいよねという話にはならない。

ちなみにnet/httpパッケージを使うとFSの静的ファイルサーバーを作れるが、ディレクトリファイルをReadDirFileとして実装しないとディレクトリの中身を読んでくれない。

fs.ReadDirFileの実装のしかた

仕様がちょっと複雑だ。 archive/zipパッケージにはfs.FSが実装されていてzipファイルを解凍せずに中身をファイルシステムとして読むことができる。 そこのReadDirメソッドのコードは実装するときに参考になるかもしれない。

https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/archive/zip/reader.go;l=884

DirEntryとFileInfoの違い

DirEntryもFileInfoもファイルの情報を保持するインターフェースである。 しかしなぜ同じようなものが2つあるのか。 ReadDirもDirEntryという劣化版ではなくて、FileInfoを返せばよいではないか。と疑問を感じるかもしれない。

これはUnixファイルシステムの設計が原因である。 Unixファイルシステムにおいてディレクトリの中身(DirEntry)とファイルの情報(FileInfo)は別々の領域に格納されている。 ディレクトリの中身を読み(readdir システムコール)、中身のファイルの情報を取得する(stat システムコール)という処理をすると2回システムコールを呼ぶ必要がある。 しかしながらディレクトリの中身を探索する処理ではたいてい DirEntry を読むだけで十分なことが多く、FileInfo を毎回呼ぶのは無駄である。 だからReadDirはFileInfoではなくDirEntryを返す実装になっている。

この経緯はGoのissueを読むとわかる。

github.com

ただオリジナルのファイルシステムを実装する際にこのような背景は知ったこっちゃないだろう。 DirEntryとFileInfoは同じ構造体で実装してしまえばよい。

type info struct {
    name    string
    size    int64
    mode    fs.FileMode
    modTime time.Time
}

var _ fs.FileInfo = &info{}
var _ fs.DirEntry = &info{}

func (i *info) Name() string               { return i.name }
func (i *info) Size() int64                { return i.size }
func (i *info) Mode() fs.FileMode          { return i.mode }
func (i *info) Type() fs.FileMode          { return i.mode.Type() }
func (i *info) ModTime() time.Time         { return i.modTime }
func (i *info) IsDir() bool                { return i.mode.IsDir() }
func (i *info) Info() (fs.FileInfo, error) { return i, nil }
func (i *info) Sys() any                   { return nil }

fs.FileInfoを実装するときに注意すること

意味のない値の扱い

/dev/urandomのようなファイルでは大きさ(Size)や更新日時(ModTime)の情報は意味をなさない。 これは個人的な意見であるが、もし作ったファイルシステムを既存の Unix ソフトウェアと組み合わせるなら、更新日時をtime.Timeのゼロ値(つまりJanuary 1, year 1, 00:00:00.000000000 UTC)にしないほうがよいかもしれない。 Unixソフトウェアを組み合わせたときにバグを引き起こしてしまうかもしれないからだ。この場合はUnixエポックタイムでのゼロ秒を返すかな。

func (info) ModTime() time.Time { return time.Unix(0,0) }