?
什么神奇問題 ?
?
Go 在文件 IO 的場(chǎng)景有個(gè)神奇的事情。打開一個(gè)文件的時(shí)候,返回的竟然不是 interface ,而是一個(gè) os.File ?結(jié)構(gòu)體的指針。
?
func?Open(name?string)?(*File,?error)?{ ????return?OpenFile(name,?O_RDONLY,?0) }
?
劃重點(diǎn):這個(gè)意味著,Go 的文件系統(tǒng)的概念和 OS 的文件系統(tǒng)的概念直接關(guān)聯(lián)起來(lái)。你必須傳入一個(gè)文件路徑,并且必須真的要去打開一個(gè)操作系統(tǒng)的文件。
不用接口,而是跟具體類型強(qiáng)相關(guān)的話,會(huì)導(dǎo)致后續(xù)的擴(kuò)展性不好。比如,全都是 os 包的使用,那么將操作強(qiáng)綁定在 OS 文件系統(tǒng)上。
最常見的,在單測(cè)的時(shí)候用的這種方式的話,就真的要在操作系統(tǒng)上打開文件做操作。Go 的設(shè)計(jì)者對(duì)此一直耿耿于懷,但是也很無(wú)奈。因?yàn)橛脩粢呀?jīng)用上了,Go 的承諾是往前兼容,直接修改原有語(yǔ)義和接口肯定不行。
怎么辦?
Go 1.16 給了我們答案。Go 給了我們一個(gè) io.FS 的封裝。Go 的意圖是在自己的語(yǔ)言層面再做一層 FS 的抽象,這樣就能和 OS 的 FS 解耦開來(lái)。io.FS 可以是任何奇形怪狀的 FS ,只要你實(shí)現(xiàn)了規(guī)定好的 FS 接口。下一步來(lái)看下 Go 1.16 帶來(lái)的幾個(gè)核心改動(dòng)。
有人說 Go 都 1.19 了,還看 1.16 ?
因?yàn)?Go 的 io/fs 是在 Go 1.16 引入的。在 io 方面有比較大的一個(gè)變化。
?
Go 1.16 關(guān)于 io 有哪些改變 ?
?
新增了一個(gè) io/fs 的包,抽象了一個(gè) FS 出來(lái)。
embed 的 package 用了這個(gè)抽象。
規(guī)整 io/ioutil 里面的內(nèi)容。
接下來(lái)我們一個(gè)個(gè)看下。
?
io.FS 的抽象
?
?1???Go 為什么要抽象 FS ?
前面已經(jīng)提到,Go 的文件系統(tǒng)的概念和 OS 的文件系統(tǒng)的概念直接關(guān)聯(lián)起來(lái)。這個(gè)給擴(kuò)展性帶來(lái)了不方便。最重要的,Go 已經(jīng)發(fā)現(xiàn)有和 OS 不同的文件系統(tǒng)的需求了,就是 embed FS 。
embed 是 Go 提供的一個(gè)打包文件到二進(jìn)制的功能,也是類似文件系統(tǒng)的一種需求。但是卻不是直接位于 OS 上的文件系統(tǒng)(vfs 那套東西)。
所以在 Go 1.16 順勢(shì)就一起上了。引入了 io.FS 的定義,并且 embed 就直接用上了這層抽象。
![[fs 封裝層次.png]]
?2???來(lái)看下 FS 接口的定義
Go 的實(shí)現(xiàn)者們很強(qiáng),推薦的是小接口。也就是最小化、原子化的接口語(yǔ)義。從 io/fs 的定義就能看到很強(qiáng)的功力。
?
//?文件系統(tǒng)的接口 type?FS?interface?{ ????Open(name?string)?(File,?error) } //?文件的接口 type?File?interface?{ ????Stat()?(FileInfo,?error) ????Read([]byte)?(int,?error) ????Close()?error }
?
這,就是最簡(jiǎn)單的 FS 。 這個(gè)就是文件系統(tǒng)極簡(jiǎn)的樣子,只需要有一個(gè) Open 方法,返回一個(gè)文件即可。
也就是說,Go 理解的文件系統(tǒng),只要能實(shí)現(xiàn)一個(gè) Open 方法,返回一個(gè) File 的 interface ,這個(gè) File 只需要實(shí)現(xiàn) Stat,Read,Close 方法即可。
有沒有發(fā)現(xiàn),OS 的 FS 已經(jīng)滿足了條件。所以,Go 的 FS 可以是 OS 的 FS ,自然也可以是其他的實(shí)現(xiàn)。
Go 在此 io.FS 的基礎(chǔ)上,再去擴(kuò)展接口,增加文件系統(tǒng)的功能。比如,加個(gè) ReadDir 就是一個(gè)有讀目錄的文件系統(tǒng) ReadDirFS :
?
type?ReadDirFS?interface?{ ????FS ????//?讀目錄 ????ReadDir(name?string)?([]DirEntry,?error) }
?
加個(gè) Glob 方法,就成為一個(gè)具備路徑通配符查詢的文件系統(tǒng):
?
type?GlobFS?interface?{ ????FS ????//?路徑通配符的功能 ????Glob(pattern?string)?([]string,?error) }
?
加個(gè) Stat ,就變成一個(gè)路徑查詢的文件系統(tǒng):
?
type?StatFS?interface?{ ????FS ????//?查詢某個(gè)路徑的文件信息 ????Stat(name?string)?(FileInfo,?error) }
?
這些非常經(jīng)典的文件系統(tǒng)的定義 Go 在 io/fs 里面已經(jīng)做好了。
?3???io.FS 怎么使用呢?
我們的目標(biāo)是實(shí)現(xiàn)一個(gè) Go 的 FS ,這個(gè)定義已經(jīng)在 io.FS 有了。我們只需要寫一個(gè)結(jié)構(gòu)體,實(shí)現(xiàn)它的方法,那么你就可以說這是一個(gè) FS 了。
這里其實(shí)就可以有非常多的想象空間,比如,可以是 OS 的 FS,也可以是 memory FS ,hash FS 等等。網(wǎng)上有不少例子。但其實(shí)標(biāo)準(zhǔn)庫(kù)已經(jīng)有一個(gè)最好的例子,那就是 embed FS 。
我們來(lái)看下 embed 怎么實(shí)現(xiàn)一個(gè)內(nèi)嵌的文件系統(tǒng)。embed 的實(shí)現(xiàn)在 embed/embed.go 這個(gè)文件中,非常精簡(jiǎn)。
首先,在 embed package 里定義了一個(gè)結(jié)構(gòu)體 FS ,這個(gè)結(jié)構(gòu)體將是 io.FS 的具體實(shí)現(xiàn)。
?
//?作為具體?FS?的實(shí)現(xiàn) type?FS?struct?{ ????files?*[]file } //?代表一個(gè)內(nèi)嵌文件 type?file?struct?{ ????name?string ????data?string??//?文件的數(shù)據(jù)全在內(nèi)存里 ????hash?[16]byte?//?truncated?SHA256?hash }
?
embed 里面的 FS 結(jié)構(gòu)體只需要實(shí)現(xiàn) Open 這個(gè)方法即可:
?
//?Open?的具體實(shí)現(xiàn) func?(f?FS)?Open(name?string)?(fs.File,?error)?{ ????//?通過名字匹配查找到?file?對(duì)象 ????file?:=?f.lookup(name) ????//?如果沒找到 ????if?file?==?nil?{ ????????return?nil,?&fs.PathError{Op:?"open",?Path:?name,?Err:?fs.ErrNotExist} ????} ????//?如果是目錄結(jié)構(gòu) ????if?file.IsDir()?{ ????????return?&openDir{file,?f.readDir(name),?0},?nil ????} ????//?找到了就封裝成?openFile?結(jié)構(gòu)體 ????return?&openFile{file,?0},?nil }
?
上面的 Open ,如果是文件的化,返回的是一個(gè) openFile 的結(jié)構(gòu)體 ,作為 io.File 接口的具體實(shí)現(xiàn):
?
//?代表一個(gè)文件的實(shí)現(xiàn) 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)?{ ????//?判斷偏移是否符合預(yù)期 ????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èi)存拷貝數(shù)據(jù) ????n?:=?copy(b,?f.f.data[f.offset:]) ????f.offset?+=?int64(n) ????return?n,?nil }
?
如上,只需要實(shí)現(xiàn) Read,Stat,Close 方法即可。這就是一個(gè)完整的、Go 層面的 FS 的實(shí)現(xiàn)。
你可以如下使用 embed 文件系統(tǒng):
?
//go:embed?hello.txt var?f?embed.FS func?main()?{ ????//?打開文件 ????file,?err?:=?f.Open("hello.txt") ????//?... ????//?讀文件 ????n,?err?=?file.Read(/*buffer*/) }
?
上面的例子,編譯的時(shí)候會(huì)把當(dāng)前目錄下的一個(gè) hello.txt 文件打包到二進(jìn)制文件。程序啟動(dòng)的時(shí)候可以把它讀出來(lái)。
注意:f 這個(gè)變量,編譯器會(huì)安排填充好。進(jìn)程啟動(dòng)時(shí)它是有值的。
?
Go 1.16 關(guān)于 IO 其他的改動(dòng)
?
除了上面提到的 io/fs 和 embed fs ,Go 對(duì)之前的 io 的一些結(jié)構(gòu)也做了更準(zhǔn)確的調(diào)整分類。把之前大雜燴的 io/ioutil 里面的東西拆出來(lái)了。移到對(duì)應(yīng)的 io 包和 os 包。為了兼容性,ioutil 包并沒有直接刪除,而是導(dǎo)入。比如:
Discard 移到了 io 庫(kù)實(shí)現(xiàn)
ReadAll 移到了 io 庫(kù)實(shí)現(xiàn)
NopCloser 移到了 io 庫(kù)實(shí)現(xiàn)
ReadFile 移到 os 庫(kù)實(shí)現(xiàn)
WriteFile 移到 os 庫(kù)實(shí)現(xiàn)
基本上 ioutil 這個(gè) package 是被掏空了。Go 1.16 只是為了兼容性還沒刪。
?
Go 的 FS 封裝有啥用呢 ?
?
好處其實(shí)很多,最明顯的兩個(gè):
單測(cè)方便了。
有類似 embed FS 這種非 OS 文件系統(tǒng)的需求,可以有方法擴(kuò)展了。
?
總結(jié)
?
Go 在自己的層面封裝出一個(gè) io.FS 的抽象,意圖和 OS 的 FS 解耦。這樣可以給程序員帶來(lái)更多的想象空間 ;
embed FS 具備典型的 FS 的界面,但是它并不是直接位于 OS 的文件系統(tǒng)。所以它非常適合作為首個(gè)用 io.FS 的實(shí)踐;
以后盡量用 io.FS 來(lái)管理的文件,這樣可以做到和 OS 解耦,方便做單測(cè);
ioutil 可以少用,它的功能已經(jīng)被移到更明確的 package 里實(shí)現(xiàn)了;
審核編輯:湯梓紅
評(píng)論
查看更多