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 的包,抽象了一個 FS 出來。
-
embed 的 package 用了這個抽象。
-
規整 io/ioutil 裏面的內容。
接下來我們一個個看下。
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(b []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 包並沒有直接刪除,而是導入。比如:
-
Discard 移到了 io 庫實現
-
ReadAll 移到了 io 庫實現
-
NopCloser 移到了 io 庫實現
-
ReadFile 移到 os 庫實現
-
WriteFile 移到 os 庫實現
基本上 ioutil 這個 package 是被掏空了。Go 1.16 只是爲了兼容性還沒刪。
Go 的 FS 封裝有啥用呢 ?
好處其實很多,最明顯的兩個:
-
單測方便了。
-
有類似 embed FS 這種非 OS 文件系統的需求,可以有方法擴展了。
總結
-
Go 在自己的層面封裝出一個 io.FS 的抽象,意圖和 OS 的 FS 解耦。這樣可以給程序員帶來更多的想象空間 ;
-
embed FS 具備典型的 FS 的界面,但是它並不是直接位於 OS 的文件系統。所以它非常適合作爲首個用 io.FS 的實踐;
-
以後儘量用 io.FS 來管理的文件,這樣可以做到和 OS 解耦,方便做單測;
-
ioutil 可以少用,它的功能已經被移到更明確的 package 裏實現了;
堅持思考,方向比努力更重要。關注我:奇伢雲存儲。歡迎加我好友,技術交流。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gYWhjh1BburgeAU72mn_aw