Go 眼中的文件系統是什麼? io-FS

什麼神奇問題 ?

Go 在文件 IO 的場景有個神奇的事情。打開一個文件的時候,返回的竟然不是 interface ,而是一個 os.File  結構體的指針。

func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

劃重點:這個意味着,Go 的文件系統的概念和 OS 的文件系統的概念直接關聯起來。你必須傳入一個文件路徑,並且必須真的要去打開一個操作系統的文件。

不用接口,而是跟具體類型強相關的話,會導致後續的擴展性不好。比如,全都是 os 包的使用,那麼將操作強綁定在 OS 文件系統上。

最常見的,在單測的時候用的這種方式的話,就真的要在操作系統上打開文件做操作。Go 的設計者對此一直耿耿於懷,但是也很無奈。因爲用戶已經用上了,Go 的承諾是往前兼容,直接修改原有語義和接口肯定不行。

怎麼辦

Go 1.16 給了我們答案。Go 給了我們一個 io.FS 的封裝。Go 的意圖是在自己的語言層面再做一層 FS 的抽象,這樣就能和 OS 的 FS 解耦開來。io.FS 可以是任何奇形怪狀的 FS ,只要你實現了規定好的 FS 接口。下一步來看下 Go 1.16 帶來的幾個核心改動。

有人說 Go 都 1.19 了,還看 1.16 ?

因爲 Go 的 io/fs 是在 Go 1.16 引入的。在 io 方面有比較大的一個變化。

Go 1.16 關於 io 有哪些改變 ?

接下來我們一個個看下。

io.FS 的抽象

 1   Go 爲什麼要抽象 FS ?

前面已經提到,Go 的文件系統的概念和 OS 的文件系統的概念直接關聯起來。這個給擴展性帶來了不方便。最重要的,Go 已經發現有和 OS 不同的文件系統的需求了,就是 embed FS 。

embed 是 Go 提供的一個打包文件到二進制的功能,也是類似文件系統的一種需求。但是卻不是直接位於 OS 上的文件系統(vfs 那套東西)。

所以在 Go 1.16 順勢就一起上了。引入了 io.FS 的定義,並且 embed 就直接用上了這層抽象。

![[fs 封裝層次. png]]

 2   來看下 FS 接口的定義

Go 的實現者們很強,推薦的是小接口。也就是最小化、原子化的接口語義。從 io/fs 的定義就能看到很強的功力。

// 文件系統的接口
type FS interface {
    Open(name string) (File, error)
}

// 文件的接口
type File interface {
    Stat() (FileInfo, error)
    Read([]byte) (int, error)
    Close() error
}

這,就是最簡單的 FS 。 這個就是文件系統極簡的樣子,只需要有一個 Open 方法,返回一個文件即可。

也就是說,Go 理解的文件系統,只要能實現一個 Open 方法,返回一個 File 的 interface ,這個 File 只需要實現 Stat,Read,Close 方法即可。

有沒有發現,OS 的 FS 已經滿足了條件。所以,Go 的 FS 可以是 OS 的 FS ,自然也可以是其他的實現。

Go 在此 io.FS 的基礎上,再去擴展接口,增加文件系統的功能。比如,加個 ReadDir 就是一個有讀目錄的文件系統 ReadDirFS :

type ReadDirFS interface {
    FS
    // 讀目錄
    ReadDir(name string) ([]DirEntry, error)
}

加個 Glob 方法,就成爲一個具備路徑通配符查詢的文件系統:

type GlobFS interface {
    FS
    // 路徑通配符的功能
    Glob(pattern string) ([]string, error)
}

加個 Stat ,就變成一個路徑查詢的文件系統:

type StatFS interface {
    FS
    // 查詢某個路徑的文件信息
    Stat(name string) (FileInfo, error)
}

這些非常經典的文件系統的定義 Go 在 io/fs 裏面已經做好了。

 3   io.FS 怎麼使用呢?

我們的目標是實現一個 Go 的 FS ,這個定義已經在 io.FS 有了。我們只需要寫一個結構體,實現它的方法,那麼你就可以說這是一個 FS 了。

這裏其實就可以有非常多的想象空間,比如,可以是 OS 的 FS,也可以是 memory FS ,hash FS 等等。網上有不少例子。但其實標準庫已經有一個最好的例子,那就是 embed FS 。

我們來看下 embed 怎麼實現一個內嵌的文件系統。embed 的實現在 embed/embed.go 這個文件中,非常精簡。

首先,在 embed package 裏定義了一個結構體 FS ,這個結構體將是 io.FS 的具體實現。

// 作爲具體 FS 的實現
type FS struct {
    files *[]file
}

// 代表一個內嵌文件
type file struct {
    name string
    data string  // 文件的數據全在內存裏
    hash [16]byte // truncated SHA256 hash
}

embed 裏面的 FS 結構體只需要實現 Open 這個方法即可:

// Open 的具體實現
func (f FS) Open(name string) (fs.File, error) {
    // 通過名字匹配查找到 file 對象
    file := f.lookup(name)
    // 如果沒找到
    if file == nil {
        return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
    }
    // 如果是目錄結構
    if file.IsDir() {
        return &openDir{file, f.readDir(name), 0}, nil
    }
    // 找到了就封裝成 openFile 結構體
    return &openFile{file, 0}, nil
}

上面的 Open ,如果是文件的化,返回的是一個 openFile 的結構體 ,作爲 io.File 接口的具體實現:

// 代表一個文件的實現
type openFile struct {
    f *file // the file itself
    offset int64 // current read offset
}
func (f *openFile) Close() error               { return nil }
func (f *openFile) Stat() (fs.FileInfo, error) { return f.f, nil }
func (f *openFile) Read([]byte) (int, error) {
    // 判斷偏移是否符合預期
    if f.offset >= int64(len(f.f.data)) {
        return 0, io.EOF
    }
    if f.offset < 0 {
        return 0, &fs.PathError{Op: "read", Path: f.f.name, Err: fs.ErrInvalid}
    }
    // 從內存拷貝數據
    n := copy(b, f.f.data[f.offset:])
    f.offset += int64(n)
    return n, nil
}

如上,只需要實現 Read,Stat,Close 方法即可。這就是一個完整的、Go 層面的 FS 的實現。

你可以如下使用 embed 文件系統:

//go:embed hello.txt
var f embed.FS

func main() {
    // 打開文件
    file, err := f.Open("hello.txt")
    // ...
    // 讀文件
    n, err = file.Read(/*buffer*/)
}

上面的例子,編譯的時候會把當前目錄下的一個 hello.txt 文件打包到二進制文件。程序啓動的時候可以把它讀出來。

注意:f 這個變量,編譯器會安排填充好。進程啓動時它是有值的。

Go 1.16 關於 IO 其他的改動

除了上面提到的 io/fs 和 embed fs ,Go 對之前的 io 的一些結構也做了更準確的調整分類。把之前大雜燴的 io/ioutil 裏面的東西拆出來了。移到對應的 io 包和 os 包。爲了兼容性,ioutil 包並沒有直接刪除,而是導入。比如:

基本上 ioutil 這個 package 是被掏空了。Go 1.16 只是爲了兼容性還沒刪。

Go 的 FS 封裝有啥用呢 ?

好處其實很多,最明顯的兩個:

總結

  1. Go 在自己的層面封裝出一個 io.FS 的抽象,意圖和 OS 的 FS 解耦。這樣可以給程序員帶來更多的想象空間 ;

  2. embed FS 具備典型的 FS 的界面,但是它並不是直接位於 OS 的文件系統。所以它非常適合作爲首個用 io.FS 的實踐;

  3. 以後儘量用 io.FS 來管理的文件,這樣可以做到和 OS 解耦,方便做單測;

  4. ioutil 可以少用,它的功能已經被移到更明確的 package 裏實現了;

堅持思考,方向比努力更重要。關注我:奇伢雲存儲。歡迎加我好友,技術交流。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/gYWhjh1BburgeAU72mn_aw