Go36-44,45-檔案操作(os.File)
os包
通過os包,可以擁有操控計算機作業系統的能力。這個程式碼包提供的都是平臺不相關的API。無論是Linux、macOS、Windows、FreeBSD、OpenBSD、Plan9,os包都可以提供統一的使用介面。這樣就可以用同樣的方法來操縱不同的作業系統,並得到相似的結果。
os包中的API主要可以幫助我們使用作業系統中的檔案系統、許可權系統、環境變數、系統程序以及系統訊號。其中,檔案系統的API最豐富。不但可以用來建立和刪除檔案以及目錄,還可以獲取到各種資訊、修改內容、修改訪問許可權。等等。這裡,最常用的資料型別就是:os.File。
os.File型別介紹
從字面上看,os.File型別代表了作業系統中的檔案,但是實際上,它代表的遠不止於此。比如對於類Unix的作業系統,包括Linux、macOS、FreeBSD等,其中的一切都可以被看作是檔案。
除了文字檔案、二進位制檔案、壓縮檔案、目錄這些常見的形式之外,還有符號連結、各種物理裝置(包括內建或外接的面向塊或者字元的裝置)、命名管道,以及套接字(也就是socket),等等。所以能夠利用os.File型別操縱的東西有很多。不過接下來主要介紹os.File型別應用於常規檔案。
實現的io介面
os.File型別擁有的都是指標方法,它的指標實現了很多io包中的介面。
對於io包中最核心的3個簡單介面io.Reader、io.Writer和io.Closer,*os.File型別都實現了它們。另外還順便實現了io包中的9個擴充套件介面中的7個。沒有實現簡單介面io.ByteReader和io.RuneReader,所以也沒有實現上面這兩個的擴充套件介面io.ByteScannser和io.RuneScanner。
總之,os.File型別及其指標型別的值,不但可以通過各種方式讀取和寫入某個檔案中的內容,還可以尋找並設定下一次讀取或寫入時的起始索引位置,另外還可以隨時對檔案進行關閉。但是,它並不能專門的讀取檔案中的下一個位元組或Unicode字元,也不能進行任何的讀回退操作。不過,單讀讀取下一個位元組或字元的功能也可以通過其他方式來實現。比如用Read方法傳入適當的引數。
使用反射檢查介面的實現下面的示例枚舉了io包中的所有介面,檢查*os.File是否實現了該介面:
package main import ( "bytes" "fmt" "io" "os" "reflect" ) // 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() { var file os.File fileType := reflect.TypeOf(&file) var buf bytes.Buffer// 存放沒有實現的那些介面資訊,最後統一打印出來 fmt.Fprintf(&buf, "Type %T not implements:\n", &file) fmt.Printf("Type %T implements:\n", &file) for _, t := range ioTypes { if fileType.Implements(t) { fmt.Println(t.String()) } else { fmt.Fprintln(&buf, t.String()) } } fmt.Println() fmt.Println(buf.String()) }
一般要檢查介面是否實現了,不需要用到反射這麼高階的用法。
操作檔案
要操作檔案,首先要獲取一個os.File型別的指標值,簡稱File值。在os包中,有如下幾個函式:
- Create
- NewFile
- Open
- OpenFile
os.Create函式
用於根據給定的路徑建立一個新的檔案。它會返回一個File值和一個錯誤值。可以在該函式返回的File值之上,對相應的檔案進行讀操作和寫操作。使用這個函式建立的檔案,對作業系統中的所有使用者來說都是可以讀和寫的。
注意,如果給定的路徑已經存在一個檔案了,那麼該函式會先清空現有檔案中的內容,然後再把該檔案的File值返回。就是覆蓋原有檔案建立一個新的空檔案。另外,如果有錯誤,會通過第二個引數返回錯誤值。比如,如果路徑不存在,那麼會返回一個*os.PathErro型別的錯誤值。
下面的示例,嘗試當在前目錄下建立一個檔案。還會把當前目錄的名稱截掉最後一個字元,這應該會是一個不存在的目錄,同樣嘗試建立一個檔案,然後會返回一個預期的錯誤:
package main import ( "fmt" "os" "path/filepath" ) func main() { tempPath := os.TempDir() fmt.Println("系統的臨時資料夾:", tempPath) fileName := "test.txt" var paths []string dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName)// 在當前資料夾下建立一個檔案 paths = append(paths, dirPath) notExistsPath := filepath.Join(dir[:len(dir)-1], fileName)// 這個資料夾路徑應該不存在 paths = append(paths, notExistsPath) for _, path := range paths { fmt.Println("建立檔案:", path) _, err := os.Create(path)// 返回的第一個引數是*File,就不要的 if err != nil { var underlyingErr string if _, ok := err.(*os.PathError); ok { underlyingErr = "(path error)" } fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr) continue } fmt.Println("建立檔案成功.") } }
os.NewFile函式
該函式在被呼叫的時候需要接受一個代表檔案描述符的uintptr型別的值,以及一個用於表示檔名的字串。如果給定的不是有效的檔案描述符,那麼會返回nil。否則,返回相應檔案的File值。這裡不要被函式名稱誤導,它的功能不是建立一個新的檔案,而是依據一個已經存在的檔案的描述符,來新建一個包裝了該檔案的File值。比如,可以像這樣拿到一個包裝了標準錯誤輸出的File值:
file := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
然後,通過這個File值向標準錯誤輸出寫入一些內容,一般的效果就是打印出錯誤資訊:
if file != nil { file.WriteString("Test Stderr.\n") }
如果是一個已存在的檔案,可以使用該檔案的檔案描述符作為函式的第一個引數返回File值。下面檔案描述符的內容裡還有一個示例。
os.Open函式
開啟一個檔案並返回包裝了該檔案的File值。該函式只能以只讀模式開啟檔案。如果呼叫了File值的任何寫入方法,都會返回錯誤:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) file, err := os.Open(dirPath) if err != nil { // 檔案可能不存在,先建立一個檔案 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } _, err = file.WriteString(" ")// 檔案是隻讀的,嘗試寫入會返回錯誤 var underlyingErr string if _, ok := err.(*os.PathError); ok { underlyingErr = "(path error)" } fmt.Fprintf(os.Stderr, "ERROR: %v %s\n", err, underlyingErr) }
檔案描述符
實際上,上面說的只讀模式,正是應用在File值所持有的檔案描述符之上的。
檔案描述符,是由通常很小的非負整數代表的。它一般會由I/O相關的系統呼叫返回,並作為某個檔案的一個表示存在。
從作業系統的層面看,針對任何檔案的I/O操作都需要用到這個檔案描述符。只不過,Go語言中的一些資料型別,為我們隱匿掉了這個描述符。實際上,在呼叫os.Create函式、os.Open函式以及之後會提到的os.OpenFile函式時,都會執行同一個系統呼叫,並且在成功之後會得到這樣一個檔案描述符。這個檔案描述符將會被儲存在返回的File值中。os.File型別有一個Fd的指標方法,返回一個uintptr型別的值。這個值就代表了當前的File值所持有的那個檔案描述符。
不過在os包中,只有NewFile函式需要用到它。所以,如果操作的只是常規的檔案或目錄,也無需特別在意。
檔案描述符相關的示例:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) file1, err := os.Open(dirPath) if err != nil { // 檔案可能不存在,先建立一個檔案 fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } file2, _ := os.Open(dirPath)// 雖然開啟的是同一個檔案,但卻是不同的檔案描述符 file3 := os.NewFile(file1.Fd(), dirPath)// 可以通過檔案描述符,獲取File值 fmt.Println(file1.Fd(), file2.Fd(), file3.Fd()) }
通過Fd方法獲取到的檔案描述符可以通過os.NewFile函式返回File值。
os.OpenFile函式
這個函式其實是os.Create函式和os.Open函式的底層支援,它最靈活。這個函式有3個引數:
func OpenFile(name string, flag int, perm FileMode) (*File, error) { testlog.Open(name) return openFileNolog(name, flag, perm) }
name引數,是檔案的路徑。
flag引數,是需要施加在檔案描述符上的模式,叫操作模式,Open函式是隻讀的就是因為在Open函式裡呼叫OpenFile的時候指定了該引數:
func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }
perm引數,也是模式,叫許可權模式。型別是os.FileMode,此型別是一個基於uint32型別的再定義型別:
type FileMode uint32
這裡的兩個模式:
- flag : 操作模式,限定了操作檔案方式
- perm : 許可權模式,控制檔案的訪問許可權
關於操作模式和訪問許可權的更多細節,在後面繼續講。
開啟檔案並寫入內容的操作示例:
package main import ( "fmt" "os" "path/filepath" ) func main() { fileName := "test.txt" dir, _ := os.Getwd() dirPath := filepath.Join(dir, fileName) // O_WRONLY:只寫模式。O_CREATE:檔案不存在就建立。O_TRUNC:開啟並清空檔案 file, err := os.OpenFile(dirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) return } n, err := file.WriteString("寫入操作") if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) } else { fmt.Println("寫入位元組數(bytes):", n) } }
操作模式
針對File值的操作模式主要有:
- 只讀模式:os.O_RNONLY
- 只寫模式:os.O_WRONLY
- 讀寫模式:os.O_RDWR
在新建一個檔案的時候,必須把這三個模式中的一個設定為此檔案的操作模式。
另外,還可以再設定額外的操作模式,選項如下:
- os.O_APPEND : 寫入時追加到現有內容的後邊
- os.O_CREARE : 當檔案不存在是,建立一個新檔案
- os.O_EXCL : 需要與os.O_CREATE一同使用,表示給定的路徑不能是一個已存在的檔案。(就是指定的檔案必須不存在,然後建立檔案)
- os.O_SYNC : 在開啟的檔案之上實施同步I/O。它會保證讀寫的內容總會與硬碟上的資料保持同步
- os.O_TRUNC : 如果檔案已存在,並且是常規檔案,就先清空其中的內容。(就是建立檔案,如果檔案存在則新建並覆蓋)
操作模式的使用
對於以上操作模式的使用,os.Open函式和os.Create函式都是現成的例子:
func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) } func Create(name string) (*File, error) { return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) }
這裡順便也是下面訪問許可權的例子了。
這裡可以看到,多個操作符是通過按位或操作符(|)組合起來的,常用的寫模式的組合還有:
os.O_WRONLY|os.O_CREATE|os.O_EXCL os.O_WRONLY|os.O_CREATE|os.O_TRUNC os.O_WRONLY|os.O_APPEND
訪問許可權
os.OpenFile函式的第三個引數perm代表的是許可權模式,型別是os.FileMode。實際上,os.FIleMode型別能夠代表的,不只許可權模式,還可以代表檔案模式,也可以稱之為檔案種類。
os.FileMode是基於uint32型別的再定義型別,它包含了32個位元位。在這32個位元位中,每個位元位都有其特定的意義:
- 如果最高位上的二進位制數是1,那麼該值的檔案模式等同於os.ModeDir,這代表是一個目錄。
- 如果第26個位元位是1,那麼該值的檔案模式等同於os.ModeNamedPipe,這代表是一個命名管道。
- 最低的9個位元位,這幾位才用於表示檔案的許可權。這個許可權參考Linux的ugo許可權。
所有的常量都在原始碼裡有說明:
const ( // The single letters are the abbreviations // used by the String method's formatting. ModeDirFileMode = 1 << (32 - 1 - iota) // d: is a directory ModeAppend// a: append-only ModeExclusive// l: exclusive use ModeTemporary// T: temporary file; Plan 9 only ModeSymlink// L: symbolic link ModeDevice// D: device file ModeNamedPipe// p: named pipe (FIFO) ModeSocket// S: Unix domain socket ModeSetuid// u: setuid ModeSetgid// g: setgid ModeCharDevice// c: Unix character device, when ModeDevice is set ModeSticky// t: sticky ModeIrregular// ?: non-regular file; nothing else is known about this file // Mask for the type bits. For regular files, none will be set. ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeIrregular ModePerm FileMode = 0777 // Unix permission bits )
可以像操作模式那樣用按位或操作符(|)組合起來。一般也就直接0666或0777就好了。
補充-檔案讀寫
這篇還是偏重理論,主要講的開啟檔案的操作。在開啟檔案獲取到os.File型別後,由於它的指標已經實現了各種io介面,之後的讀寫操作,就是通過呼叫*os.File實現的io介面來實現了。另外,io包還有一個ioutil子包,可以讀取整個檔案。而逐行讀取檔案的內容,也需要一些具體的實現。
下面這篇有一些檔案讀寫的示例:
http://blog.51cto.com/steed/2315597