Go語言核心36講(Go語言實戰與應用二十二)--學習筆記

MingsonZheng發表於2021-12-06

44 | 使用os包中的API (上)

我們今天要講的是os程式碼包中的 API。這個程式碼包可以讓我們擁有操控計算機作業系統的能力。

前導內容: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值:

file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")

然後,通過這個File值向標準錯誤輸出上寫入一些內容:

if file3 != nil {
 defer file3.Close()
 file3.WriteString(
  "The Go language program writes the contents into stderr.\n")
}

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指代的模式叫做許可權模式。可以這麼說,操作模式限定了操作檔案的方式,而許可權模式則可以控制檔案的訪問許可權。關於許可權模式的更多細節我們將在後面討論。

image

(獲得 os.File 型別的指標值的幾種方式)

到這裡,你需要記住的是,通過os.File型別的值,我們不但可以對檔案進行讀取、寫入、關閉等操作,還可以設定下一次讀取或寫入時的起始索引位置。

此外,os包中還有用於建立全新檔案的Create函式,用於包裝現存檔案的NewFile函式,以及可被用來開啟已存在的檔案的Open函式和OpenFile函式。

package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"syscall"
)

// ioTypes 代表了io程式碼包中的所有介面的反射型別。
var ioTypes = []reflect.Type{
	reflect.TypeOf((*io.Reader)(nil)).Elem(),
	reflect.TypeOf((*io.Writer)(nil)).Elem(),
	reflect.TypeOf((*io.Closer)(nil)).Elem(),

	reflect.TypeOf((*io.ByteReader)(nil)).Elem(),
	reflect.TypeOf((*io.RuneReader)(nil)).Elem(),
	reflect.TypeOf((*io.ReaderAt)(nil)).Elem(),
	reflect.TypeOf((*io.Seeker)(nil)).Elem(),
	reflect.TypeOf((*io.WriterTo)(nil)).Elem(),
	reflect.TypeOf((*io.ByteWriter)(nil)).Elem(),
	reflect.TypeOf((*io.WriterAt)(nil)).Elem(),
	reflect.TypeOf((*io.ReaderFrom)(nil)).Elem(),

	reflect.TypeOf((*io.ByteScanner)(nil)).Elem(),
	reflect.TypeOf((*io.RuneScanner)(nil)).Elem(),
	reflect.TypeOf((*io.ReadSeeker)(nil)).Elem(),
	reflect.TypeOf((*io.ReadCloser)(nil)).Elem(),
	reflect.TypeOf((*io.WriteCloser)(nil)).Elem(),
	reflect.TypeOf((*io.WriteSeeker)(nil)).Elem(),
	reflect.TypeOf((*io.ReadWriter)(nil)).Elem(),
	reflect.TypeOf((*io.ReadWriteSeeker)(nil)).Elem(),
	reflect.TypeOf((*io.ReadWriteCloser)(nil)).Elem(),
}

func main() {
	// 示例1。
	file1 := (*os.File)(nil)
	fileType := reflect.TypeOf(file1)
	var buf bytes.Buffer
	fmt.Fprintf(&buf, "Type %T implements\n", file1)
	for _, t := range ioTypes {
		if fileType.Implements(t) {
			buf.WriteString(t.String())
			buf.WriteByte(',')
			buf.WriteByte('\n')
		}
	}
	output := buf.Bytes()
	output[len(output)-2] = '.'
	fmt.Printf("%s\n", output)

	// 示例2。
	fileName1 := "something1.txt"
	filePath1 := filepath.Join(os.TempDir(), fileName1)
	var paths []string
	paths = append(paths, filePath1)
	dir, _ := os.Getwd()
	paths = append(paths, filepath.Join(dir[:len(dir)-1], fileName1))
	for _, path := range paths {
		fmt.Printf("Create a file with path %s ...\n", path)
		_, err := os.Create(path)
		if err != nil {
			var underlyingErr string
			if _, ok := err.(*os.PathError); ok {
				underlyingErr = "(path error)"
			}
			fmt.Printf("error: %v %s\n", err, underlyingErr)
			continue
		}
		fmt.Println("The file has been created.")
	}
	fmt.Println()

	// 示例3。
	fmt.Println("New a file associated with stderr ...")
	file3 := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
	if file3 != nil {
		file3.WriteString(
			"The Go language program writes something to stderr.\n")
	}
	fmt.Println()

	// 示例4。
	fmt.Printf("Open a file with path %s ...\n", filePath1)
	file4, err := os.Open(filePath1)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}
	fmt.Println("Write something to the file ...")
	_, err = file4.WriteString("something")
	var underlyingErr string
	if _, ok := err.(*os.PathError); ok {
		underlyingErr = "(path error)"
	}
	fmt.Printf("error: %v %s\n", err, underlyingErr)
	fmt.Println()

	// 示例5。
	fmt.Printf("Open a file with path %s ...\n", filePath1)
	file5a, err := os.Open(filePath1)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}
	fmt.Printf(
		"Is there only one file descriptor for the same file in the same process? %v\n",
		file5a.Fd() == file4.Fd())
	file5b := os.NewFile(file5a.Fd(), filePath1)
	fmt.Printf("Can the same file descriptor represent the same file? %v\n",
		file5b.Name() == file5a.Name())
	fmt.Println()

	// 示例6。
	fmt.Printf("Reuse a file on path %s ...\n", filePath1)
	file6, err := os.OpenFile(filePath1, os.O_WRONLY|os.O_TRUNC, 0666)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}
	contents := "something"
	fmt.Printf("Write %q to the file ...\n", contents)
	n, err := file6.WriteString(contents)
	if err != nil {
		fmt.Printf("error: %v\n", err)
	} else {
		fmt.Printf("The number of bytes written is %d.\n", n)
	}
}

總結

我們今天講的是os程式碼包以及其中的程式實體。我們首先討論了os包存在的意義,和它的主要用途。程式碼包中所包含的 API,都是對作業系統的某方面功能的高層次抽象,這使得我們可以通過它以統一的方式,操縱不同的作業系統,並得到相似的結果。

在這個程式碼包中,操縱檔案系統的 API 最為豐富,最有代表性的就是資料型別os.File。os.File型別不但可以代表作業系統中的檔案,還可以代表很多其他的東西。尤其是在類 Unix 的作業系統中,它幾乎可以代表一切可以操縱的軟體和硬體。

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章