Go 檔案操作詳解
Go 在 os 中提供了檔案的基本操作,包括通常意義的開啟、建立、讀寫等操作,除此以外為了追求便捷以及效能上,Go 還在 io/ioutil 以及 bufio 提供一些其他函式供開發者使用,今天在這篇文章中,我們介紹一些常用檔案操作在 Go 中是如何使用的。
File 檔案型別
Go 在 os 中定義了 File 型別:
type File struct { // contains filtered or unexported fields }
開啟一個檔案進行讀直接使用 os.Open
:
file, err := os.Open("msg.txt")
os.Open
只接受一個檔名引數,預設開啟的檔案只支援讀操作,檔案的讀寫 flag
是以常量的形式定義的 Constants 分別是:
const ( // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified. O_RDONLY int = syscall.O_RDONLY // open the file read-only. O_WRONLY int = syscall.O_WRONLY // open the file write-only. O_RDWRint = syscall.O_RDWR// open the file read-write. // The remaining values may be or'ed in to control behavior. O_APPEND int = syscall.O_APPEND // append data to the file when writing. O_CREATE int = syscall.O_CREAT// create a new file if none exists. O_EXCLint = syscall.O_EXCL// used with O_CREATE, file must not exist. O_SYNCint = syscall.O_SYNC// open for synchronous I/O. O_TRUNCint = syscall.O_TRUNC// truncate regular writable file when opened. )
以 os.Open
開啟的檔案其實就只有 O_RDONLY
flag。
檔案讀取
讀取檔案操作時通過 File
的方法 Read 進行的,這個方法接受一個引數 buf []byte
,預設讀取的內容大小是 len(buf)
,並且返回讀取的位元組 size 和錯誤(如果有的話),如果讀取到了檔案末尾,則返回 0以及 io.EOF
。
if err != nil { fmt.Println(err) } buf := make([]byte, 126) n, err := file.Read(buf) if err != nil { fmt.Println(err) } fmt.Printf("%d = %q", n, buf)
按行讀取
在大多數檔案操作中,我們可能只需要的一行行讀取檔案就可以滿足需要,在 Go 中如何讀取行呢?至少在 os 這個 package 中好像沒有找到相關操作,其實 Go 已經在其他包中提供了這個操作 bufio 。
bufio 顧名思義就是帶 buffer 的 IO,由於頻繁讀寫磁碟會有相當的效能開銷,因為一次磁碟的讀寫就是一次系統的呼叫,所以 Go 提供了一個 buffer 來緩衝讀寫的資料,比如多次寫磁碟 bufio 就會把資料先緩衝起來,待 buffer 裝滿之後一次性寫入,又比如多次讀資料,bufio 會預先按照 buffer 的大小(一般是磁碟 block size 的整數倍)儘量多的讀取資料,也就是採用預讀的技術以提高讀的效能。
bufio 提供了 Reader 、 Writer 、 Scanner 來進行檔案的讀寫,其中 Reader 和 Scanner 都支援按行讀取檔案。
Reader 讀取行
使用 Reader 的 ReadLine 按行讀,其中 file 表示我們剛才開啟的檔案:
reader := bufio.NewReader(file) buf, _, err = reader.ReadLine()
ReadLine 讀取檔案的一行,預設是以 \r\n
或者 \n
分割,並且不包括分割符,如果行太長超過了內部 buffer 的大小,第二個返回值 isPrefix 就會被設定,直到 isPrefix 為 false 為止,表示一行讀取完成。
除了 ReadLine 之外, ReadBytes 也支援按行讀取,區別是 ReadBytes 需要顯示的指定分隔符,而且其返回的資料中包括分割符:
buf, err = reader.ReadBytes('\n') fmt.Printf("%d = %q", len(buf), buf) //輸出包含 \n
除了對行的讀取,bufio.Reader 還包含 ReadRune 、ReadSlice、ReadString 等讀取內容的函式。
Scanner 讀取行
Scanner 其實類似於 Reader,但是 scanner 有更強的便捷性,scanner 的主要目的就是利用各種分隔符來讀取行,他提供了 SplitFunc 來自定義對檔案內容的分割:
scanner := bufio.NewScanner(file) for scanner.Scan() { fmt.Println(scanner.Text()) }
上面的程式碼會把檔案 file 的內容按行輸出,為什麼恰好會按行輸出?主要原因是 scanner 提供的預設的 SplitFunc 是 ScanLines ,也就是 scanner.Text() 方法使用就是這個 splitfunc。
接下類我們使用一個自定義的 SplitFunc 來實現從文字中找到可以轉換成數字的字元。
r := strings.NewReader("123 456 k789 123") split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { // scanWords 按照 space 進行分割 advance, token, err = bufio.ScanWords(data, atEOF) fmt.Printf("data=%q\n", data) fmt.Printf("advance=%d\n", advance) fmt.Printf("token=%q\n", token) fmt.Printf("atEOF=%t\n", atEOF) if strings.Trim(string(token), " ") != "" { _, err = strconv.ParseInt(string(token), 10, 32) } return } scanner := bufio.NewScanner(r) scanner.Split(split) for scanner.Scan() { fmt.Println("scan text=", scanner.Text()) fmt.Println("=======") } if err := scanner.Err(); err != nil { fmt.Printf("%s", err) }
上面的例子中我們定義了一個 SplitFunc,正如 SplitFunc 簽名一樣,他接受三個引數,分別是待處理的資料 data,是否還有更多的資料要處理的標識 atEOF,然後返回的是當前已經處理的資料的位元組長度 advance,已經處理的位元組陣列 token,以及一個可選的錯誤 err。
advance 的計算是從當前剩下要處理的資料首位 0 的位置開始一直到下一個分割符,並且包含分隔符佔用的位元組,可以對照看以下輸出就能明白:
data="123 456 k789 123" advance=4//從 1 開始直到下一個空格 token="123" atEOF=false scan text= 123 ======= data="456 k789 123" advance=4 token="456" atEOF=false scan text= 456 ======= data="k789 123" advance=5//從 k 開始直到下一個空格 token="k789" atEOF=false strconv.ParseInt: parsing "k789": invalid syntax%
而且需要注意的是,scanner 在遇到一個錯誤之後就停止 Scan 了,上面的 ParseInt 發生錯誤之後之後的 Scan 也不會輸出。
File 型別和 bufio
如圖 File 是實現了 io.Reader
和 io.Writer
兩個 interface 的 type,而 bufio 提供的幾種操作都以這兩個 interface 為基礎實現檔案的讀寫,也就是說只要 type 實現了 io.Reader 就可以使用 bufio 讀取,實現了 io.Writer 就可以使用 bufio 輸出。
str := strings.NewReader(strings.Repeat("ab", 10)) buf := make([]byte, 2) reader := bufio.NewReader(str)
如上程式碼 str 是一個 string 的 Reader,然後就可以使用 bufio進行高效讀取。
檔案的輸出
檔案的寫入類似檔案的讀取,Go 提供了 Create 、 OpenFile 開啟檔案進行寫入或追加。
Create 會開啟一個檔案,預設的模式是 O_RDWR 即讀和寫,如果原來的檔案已經存在則清空,如果不存在則新建立一個。
file, err := os.Create("new.txt") if err != nil { fmt.Println(err) } defer file.Close() file.WriteString(time.Now().Local().String())
OpenFile 提供了更靈活的方式開啟一個檔案,他接受三個引數,依次是檔名,開啟檔案的 flag,以及檔案許可權。
file, err := os.OpenFile("new.txt", os.O_RDWR|os.O_CREATE, 0775) if err != nil { fmt.Println(err) } defer file.Close() file.WriteString(time.Now().Local().String())
除了 WriteString,file 型別還提供了 Write 方法,區別是 Write 接受的是 []byte 。
使用 bufio.Writer 進行檔案輸出
上面我們提到過 bufio 提供了 Writer 來進行高效的輸出,如何使用呢?
Writer 實際上是一個內部包含 buffer 的特殊 struct,其結構大致如下:
type Writer struct { err error buf []byte nint wrio.Writer }
buf 這個 field 就是緩衝輸出內容的,當滿足指定 size 之後,Writer 才會把 buf 中的內容通過 wr 寫到輸出物件。
wr := bufio.NewWriterSize(os.Stdout, 38) count := 0 for { wr.WriteString(time.Now().Format("2006-01-02 15:04:05")) time.Sleep(time.Second * 1) fmt.Println("\ncount ", count) count++ if count > 10 { break } } wr.Flush()
上面的程式碼會在 buf 的 size 滿足 38 之後輸出到標準輸出,可以執行程式碼檢視輸出時間隔 2 秒產生的:
count0 count1 2019-03-10 14:01:022019-03-10 14:01:03 count2 count3 2019-03-10 14:01:042019-03-10 14:01:05 count4 count5 2019-03-10 14:01:062019-03-10 14:01:07 count6 count7 2019-03-10 14:01:082019-03-10 14:01:09 count8 count9 2019-03-10 14:01:102019-03-10 14:01:11 count10 2019-03-10 14:01:12
預設情況下 bufio.Writer 指定的 size 大小是 defaultBufSize = 4096,像上面的程式碼一樣可以通過 NewWriterSize 來改變這個大小。
需要注意的是,Writer 在遇到錯誤之後不會接著執行後面的輸出,看以下程式碼:
type Writer int func (*Writer) Write(p []byte) (n int, err error) { fmt.Printf("Write: %q\n", p) return 0, errors.New("IO Error!") } func main() { wr := bufio.NewWriterSize(new(Writer), 3) wr.Write([]byte{'a'}) wr.Write([]byte{'b'}) wr.Write([]byte{'c'}) wr.Write([]byte{'d'}) err := wr.Flush() fmt.Println(err) }
輸出:
Write: "abc" IO Error!
最後一個字元 d 沒有輸出
ioutil 包的檔案讀寫
除了上面提到的對檔案的讀寫操作, io/ioutil 中提供了幾個便捷的函式來讀寫檔案,分別是: