我們今天要講的是os代碼包中的 API。這個代碼包可以讓我們擁有操控計算機作業系統的能力。
這個代碼包提供的都是平台不相關的 API。那麼說,什麼叫平台不相關的 API 呢?
它的意思是:這些 API 基于(或者說抽象自)作業系統,為我們使用作業系統的功能提供高層次的支援,但是,它們并不依賴于具體的作業系統。
不論是 Linux、macOS、Windows,還是 FreeBSD、OpenBSD、Plan9,os代碼包都可以為之提供統一的使用接口。這使得我們可以用同樣的方式,來操縱不同的作業系統,并得到相似的結果。
os包中的 API 主要可以幫助我們使用作業系統中的檔案系統、權限系統、環境變量、系統程序以及系統信号。
其中,操縱檔案系統的 API 最為豐富。我們不但可以利用這些 API 建立和删除檔案以及目錄,還可以擷取到它們的各種資訊、修改它們的内容、改變它們的通路權限,等等。
說到這裡,就不得不提及一個非常常用的資料類型:os.File。
從字面上來看,os.File類型代表了作業系統中的檔案。但實際上,它可以代表的遠不止于此。或許你已經知道,對于類 Unix 的作業系統(包括 Linux、macOS、FreeBSD 等),其中的一切都可以被看做是檔案。
除了文本檔案、二進制檔案、壓縮檔案、目錄這些常見的形式之外,還有符号連結、各種實體裝置(包括内置或外接的面向塊或者字元的裝置)、命名管道,以及套接字(也就是 socket),等等。
是以,可以說,我們能夠利用os.File類型操縱的東西太多了。不過,為了聚焦于os.File本身,同時也為了讓本文講述的内容更加通用,我們在這裡主要把os.File類型應用于正常的檔案。
下面這個問題,就是以os.File類型代表的最基本内容入手。我們今天的問題是:os.File類型都實作了哪些io包中的接口?
這道題的典型回答是這樣的。
os.File類型擁有的都是指針方法,是以除了空接口之外,它本身沒有實作任何接口。而它的指針類型則實作了很多io代碼包中的接口。
首先,對于io包中最核心的 3 個簡單接口io.Reader、io.Writer和io.Closer,*os.File類型都實作了它們。
其次,該類型還實作了另外的 3 個簡單接口,即:io.ReaderAt、io.Seeker和io.WriterAt。
正是因為*os.File類型實作了這些簡單接口,是以它也順便實作了io包的 9 個擴充接口中的 7 個。
然而,由于它并沒有實作簡單接口io.ByteReader和io.RuneReader,是以它沒有實作分别作為這兩者的擴充接口的io.ByteScanner和io.RuneScanner。
總之,os.File類型及其指針類型的值,不但可以通過各種方式讀取和寫入某個檔案中的内容,還可以尋找并設定下一次讀取或寫入時的起始索引位置,另外還可以随時對檔案進行關閉。
但是,它們并不能專門地讀取檔案中的下一個位元組,或者下一個 Unicode 字元,也不能進行任何的讀回退操作。
不過,單獨讀取下一個位元組或字元的功能也可以通過其他方式來實作,比如,調用它的Read方法并傳入适當的參數值就可以做到這一點。
這個問題其實在間接地問“os.File類型能夠以何種方式操作檔案?”我在前面的典型回答中也給出了簡要的答案。
在我進一步地說明一些細節之前,我們先來看看,怎樣才能獲得一個os.File類型的指針值(以下簡稱File值)。
在os包中,有這樣幾個函數,即:Create、NewFile、Open和OpenFile。
os.Create函數用于根據給定的路徑建立一個新的檔案。 它會傳回一個File值和一個錯誤值。我們可以在該函數傳回的File值之上,對相應的檔案進行讀操作和寫操作。
不但如此,我們使用這個函數建立的檔案,對于作業系統中的所有使用者來說,都是可以讀和寫的。
換句話說,一旦這樣的檔案被建立出來,任何能夠登入其所屬的作業系統的使用者,都可以在任意時刻讀取該檔案中的内容,或者向該檔案寫入内容。
注意,如果在我們給予os.Create函數的路徑之上,已經存在了一個檔案,那麼該函數會先清空現有檔案中的全部内容,然後再把它作為第一個結果值傳回。
另外,os.Create函數是有可能傳回非nil的錯誤值的。比如,如果我們給定的路徑上的某一級父目錄并不存在,那麼該函數就會傳回一個*os.PathError類型的錯誤值,以表示“不存在的檔案或目錄”。
再來看os.NewFile函數。 該函數在被調用的時候,需要接受一個代表檔案描述符的、uintptr類型的值,以及一個用于表示檔案名的字元串值。
如果我們給定的檔案描述符并不是有效的,那麼這個函數将會傳回nil,否則,它将會傳回一個代表了相應檔案的File值。
注意,不要被這個函數的名稱誤導了,它的功能并不是建立一個新的檔案,而是依據一個已經存在的檔案的描述符,來建立一個包裝了該檔案的File值。
例如,我們可以像這樣拿到一個包裝了标準錯誤輸出的File值:
然後,通過這個File值向标準錯誤輸出上寫入一些内容:
os.Open函數會打開一個檔案并傳回包裝了該檔案的File值。 然而,該函數隻能以隻讀模式打開檔案。換句話說,我們隻能從該函數傳回的File值中讀取内容,而不能向它寫入任何内容。
如果我們調用了這個File值的任何一個寫入方法,那麼都将會得到一個表示了“壞的檔案描述符”的錯誤值。實際上,我們剛剛說的隻讀模式,正是應用在File值所持有的檔案描述符之上的。
所謂的檔案描述符,是由通常很小的非負整數代表的。它一般會由 I/O 相關的系統調用傳回,并作為某個檔案的一個辨別存在。
從作業系統的層面看,針對任何檔案的 I/O 操作都需要用到這個檔案描述符。隻不過,Go 語言中的一些資料類型,為我們隐匿掉了這個描述符,如此一來我們就無需時刻關注和辨識它了(就像os.File類型這樣)。
實際上,我們在調用前文所述的os.Create函數、os.Open函數以及将會提到的os.OpenFile函數的時候,它們都會執行同一個系統調用,并且在成功之後得到這樣一個檔案描述符。這個檔案描述符将會被儲存在它們傳回的File值中。
os.File類型有一個指針方法,名叫Fd。它在被調用之後将會傳回一個uintptr類型的值。這個值就代表了目前的File值所持有的那個檔案描述符。
不過,在os包中,除了NewFile函數需要用到它,它也沒有什麼别的用武之地了。是以,如果你操作的隻是正常的檔案或者目錄,那麼就無需特别地在意它了。
最後,再說一下os.OpenFile函數。 這個函數其實是os.Create函數和os.Open函數的底層支援,它最為靈活。
這個函數有 3 個參數,分别名為name、flag和perm。其中的name指代的就是檔案的路徑。而flag參數指的則是需要施加在檔案描述符之上的模式,我在前面提到的隻讀模式就是這裡的一個可選項。
在 Go 語言中,這個隻讀模式由常量os.O_RDONLY代表,它是int類型的。當然了,這裡除了隻讀模式之外,還有幾個别的模式可選,我們稍後再細說。
os.OpenFile函數的參數perm代表的也是模式,它的類型是os.FileMode,此類型是一個基于uint32類型的再定義類型。
為了加以差別,我們把參數flag指代的模式叫做操作模式,而把參數perm指代的模式叫做權限模式。可以這麼說,操作模式限定了操作檔案的方式,而權限模式則可以控制檔案的通路權限。關于權限模式的更多細節我們将在後面讨論。

(獲得 os.File 類型的指針值的幾種方式)
到這裡,你需要記住的是,通過os.File類型的值,我們不但可以對檔案進行讀取、寫入、關閉等操作,還可以設定下一次讀取或寫入時的起始索引位置。
此外,os包中還有用于建立全新檔案的Create函數,用于包裝現存檔案的NewFile函數,以及可被用來打開已存在的檔案的Open函數和OpenFile函數。
我們今天講的是os代碼包以及其中的程式實體。我們首先讨論了os包存在的意義,和它的主要用途。代碼包中所包含的 API,都是對作業系統的某方面功能的高層次抽象,這使得我們可以通過它以統一的方式,操縱不同的作業系統,并得到相似的結果。
在這個代碼包中,操縱檔案系統的 API 最為豐富,最有代表性的就是資料類型os.File。os.File類型不但可以代表作業系統中的檔案,還可以代表很多其他的東西。尤其是在類 Unix 的作業系統中,它幾乎可以代表一切可以操縱的軟體和硬體。
https://github.com/MingsonZheng/go-core-demo
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。
歡迎轉載、使用、重新釋出,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。