?
什么神奇問題 ?
?
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 里實現了;
審核編輯:湯梓紅
評論
查看更多