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

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 的操作系統中,它幾乎可以代表一切可以操縱的軟件和硬件。

筆記源碼

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

知識共享許可協議

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

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