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_RDWR   int = 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_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
        O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
        O_TRUNC  int = 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
    n   int
    wr  io.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 秒產生的:

count  0
count  1
2019-03-10 14:01:022019-03-10 14:01:03
count  2
count  3
2019-03-10 14:01:042019-03-10 14:01:05
count  4
count  5
2019-03-10 14:01:062019-03-10 14:01:07
count  6
count  7
2019-03-10 14:01:082019-03-10 14:01:09
count  8
count  9
2019-03-10 14:01:102019-03-10 14:01:11
count  10
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 中提供了幾個便捷的函數來讀寫文件,分別是:

WriteFile 、ReadFile ,他們可以直接對文件進行寫入和讀取,省去了一個打開的過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章