文件讀取是所有編程語言中最常見的操作之一。本教程我們會學習如何使用 Go 讀取文件。
本教程分為如下小節。
- 將整個文件讀取到內存
- 使用絕對文件路徑
- 使用命令行標記來傳遞文件路徑
- 將文件綁定在二進制文件中
- 分塊讀取文件
- 逐行讀取文件
將整個文件讀取到內存
將整個文件讀取到內存是最基本的文件操作之一。這需要使用 ioutil
包中的 ReadFile
函數。
讓我們在 Go 程序所在的目錄中,讀取一個文件。我已經在 GOPATH(譯注:原文是 GOROOT,應該是筆誤)中創建了文件夾,在該文件夾內部,有一個文本文件 test.txt
,我們會使用 Go 程序 filehandling.go
來讀取它。test.txt
包含文本 “Hello World. Welcome to file handling in Go”。我的文件夾結構如下:
src
filehandling
filehandling.go
test.txt
接下來我們來看看代碼。
package main import ( "fmt" "io/ioutil" ) func main() { data, err := ioutil.ReadFile("test.txt") if err != nil { fmt.Println("File reading error", err) return } fmt.Println("Contents of file:", string(data)) }
由於無法在 playground 上讀取文件,因此請在你的本地環境運行這個程序。
在上述程序的第 9 行,程序會讀取文件,並返回一個字節切片,而這個切片保存在 data
中。在第 14 行,我們將 data
轉換為 string
,顯示出文件的內容。
請在 test.txt 所在的位置運行該程序。
例如,對於 linux/mac,如果 test.txt 位於 /home/naveen/go/src/filehandling,可以使用下列步驟來運行程序。
$ cd /home/naveen/go/src/filehandling/ $ go install filehandling $ workspacepath/bin/filehandling
對於 windows,如果 test.txt 位於 C:\Users\naveen.r\go\src\filehandling,則使用下列步驟。
> cd C:\Users\naveen.r\go\src\filehandling > go install filehandling > workspacepath\bin\filehandling.exe
該程序會輸出:
Contents of file: Hello World. Welcome to file handling in Go.
如果在其他位置運行這個程序(比如 /home/userdirectory
),會打印下面的錯誤。
File reading error open test.txt: The system cannot find the file specified.
這是因為 Go 是編譯型語言。go install
會根據源代碼創建一個二進制文件。二進制文件獨立於源代碼,可以在任何位置上運行。由於在運行二進制文件的位置上沒有找到 test.txt
,因此程序會報錯,提示無法找到指定的文件。
有三種方法可以解決這個問題。
- 使用絕對文件路徑
- 使用命令行標記來傳遞文件路徑
- 將文件綁定在二進制文件中
讓我們來依次介紹。
1. 使用絕對文件路徑
要解決問題,最簡單的方法就是傳入絕對文件路徑。我已經修改了程序,把路徑改成了絕對路徑。
package main import ( "fmt" "io/ioutil" ) func main() { data, err := ioutil.ReadFile("/home/naveen/go/src/filehandling/test.txt") if err != nil { fmt.Println("File reading error", err) return } fmt.Println("Contents of file:", string(data)) }
現在可以在任何位置上運行程序,打印出 test.txt
的內容。
例如,可以在我的家目錄運行。
$ cd $HOME $ go install filehandling $ workspacepath/bin/filehandling
該程序打印出了 test.txt
的內容。
看似這是一個簡單的方法,但它的缺點是:文件必須放在程序指定的路徑中,否則就會出錯。
2. 使用命令行標記來傳遞文件路徑
另一種解決方案是使用命令行標記來傳遞文件路徑。使用 flag 包,我們可以從輸入的命令行獲取到文件路徑,接着讀取文件內容。
首先我們來看看 flag
包是如何工作的。flag
包有一個名為 String
的函數。該函數接收三個參數。第一個參數是標記名,第二個是默認值,第三個是標記的簡短描述。
讓我們來編寫程序,從命令行讀取文件名。將 filehandling.go
的內容替換如下:
package main import ( "flag" "fmt" ) func main() { fptr := flag.String("fpath", "test.txt", "file path to read from") flag.Parse() fmt.Println("value of fpath is", *fptr) }
在上述程序中第 8 行,通過 String
函數,創建了一個字符串標記,名稱是 fpath
,默認值是 test.txt
,描述為 file path to read from
。這個函數返回存儲 flag 值的字符串變量的地址。
在程序訪問 flag 之前,必須先調用 flag.Parse()
。
在第 10 行,程序會打印出 flag 值。
使用下面命令運行程序。
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
我們傳入 /path-of-file/test.txt
,賦值給了 fpath
標記。
該程序輸出:
value of fpath is /path-of-file/test.txt
這是因為 fpath
的默認值是 test.txt
。
現在我們知道如何從命令行讀取文件路徑了,讓我們繼續完成我們的文件讀取程序。
package main import ( "flag" "fmt" "io/ioutil" ) func main() { fptr := flag.String("fpath", "test.txt", "file path to read from") flag.Parse() data, err := ioutil.ReadFile(*fptr) if err != nil { fmt.Println("File reading error", err) return } fmt.Println("Contents of file:", string(data)) }
在上述程序里,命令行傳入文件路徑,程序讀取了該文件的內容。使用下面命令運行該程序。
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
請將 /path-of-file/
替換為 test.txt
的真實路徑。該程序將打印:
Contents of file: Hello World. Welcome to file handling in Go.
3. 將文件綁定在二進制文件中
雖然從命令行獲取文件路徑的方法很好,但還有一種更好的解決方法。如果我們能夠將文本文件捆綁在二進制文件,豈不是很棒?這就是我們下面要做的事情。
有很多包可以幫助我們實現。我們會使用 packr,因為它很簡單,並且我在項目中使用它時,沒有出現任何問題。
第一步就是安裝 packr
包。
在命令提示符中輸入下面命令,安裝 packr
包。
go get -u github.com/gobuffalo/packr/...
packr
會把靜態文件(例如 .txt
文件)轉換為 .go
文件,接下來,.go
文件會直接嵌入到二進制文件中。packer
非常智能,在開發過程中,可以從磁盤而非二進制文件中獲取靜態文件。在開發過程中,當僅僅靜態文件變化時,可以不必重新編譯。
我們通過程序來更好地理解它。用以下內容來替換 handling.go
文件。
package main import ( "fmt" "github.com/gobuffalo/packr" ) func main() { box := packr.NewBox("../filehandling") data := box.String("test.txt") fmt.Println("Contents of file:", data) }
在上面程序的第 10 行,我們創建了一個新盒子(New Box)。盒子表示一個文件夾,其內容會嵌入到二進制中。在這里,我指定了 filehandling
文件夾,其內容包含 test.txt
。在下一行,我們讀取了文件內容,並打印出來。
在開發階段時,我們可以使用 go install
命令來運行程序。程序可以正常運行。packr
非常智能,在開發階段可以從磁盤加載文件。
使用下面命令來運行程序。
go install filehandling workspacepath/bin/filehandling
該命令可以在其他位置運行。packr
很聰明,可以獲取傳遞給 NewBox
命令的目錄的絕對路徑。
該程序會輸出:
Contents of file: Hello World. Welcome to file handling in Go.
你可以試着改變 test.txt
的內容,然后再運行 filehandling
。可以看到,無需再次編譯,程序打印出了 test.txt
的更新內容。完美!
現在我們來看看如何將 test.txt
打包到我們的二進制文件中。我們使用 packr
命令來實現。
運行下面的命令:
packr install -v filehandling
它會打印:
building box ../filehandling packing file filehandling.go packed file filehandling.go packing file test.txt packed file test.txt built box ../filehandling with ["filehandling.go" "test.txt"] filehandling
該命令將靜態文件綁定到了二進制文件中。
在運行上述命令之后,使用命令 workspacepath/bin/filehandling
來運行程序。程序會打印出 test.txt
的內容。於是從二進制文件中,我們讀取了 test.txt
的內容。
如果你不知道文件到底是由二進制還是磁盤來提供,我建議你刪除 test.txt
,並在此運行 filehandling
命令。你將看到,程序打印出了 test.txt
的內容。太棒了:D。我們已經成功將靜態文件嵌入到了二進制文件中。
分塊讀取文件
在前面的章節,我們學習了如何把整個文件讀取到內存。當文件非常大時,尤其在 RAM 存儲量不足的情況下,把整個文件都讀入內存是沒有意義的。更好的方法是分塊讀取文件。這可以使用 bufio 包來完成。
讓我們來編寫一個程序,以 3 個字節的塊為單位讀取 test.txt
文件。如下所示,替換 filehandling.go
的內容。
package main import ( "bufio" "flag" "fmt" "log" "os" ) func main() { fptr := flag.String("fpath", "test.txt", "file path to read from") flag.Parse() f, err := os.Open(*fptr) if err != nil { log.Fatal(err) } defer func() { if err = f.Close(); err != nil { log.Fatal(err) } }() r := bufio.NewReader(f) b := make([]byte, 3) for { _, err := r.Read(b) if err != nil { fmt.Println("Error reading file:", err) break } fmt.Println(string(b)) } }
在上述程序的第 15 行,我們使用命令行標記傳遞的路徑,打開文件。
在第 19 行,我們延遲了文件的關閉操作。
在上面程序的第 24 行,我們新建了一個緩沖讀取器(buffered reader)。在下一行,我們創建了長度和容量為 3 的字節切片,程序會把文件的字節讀取到切片中。
第 27 行的 Read
方法會讀取 len(b) 個字節(達到 3 字節),並返回所讀取的字節數。當到達文件最后時,它會返回一個 EOF 錯誤。程序的其他地方比較簡單,不做解釋。
如果我們使用下面命令來運行程序:
$ go install filehandling $ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
會得到以下輸出:
Hel
lo
Wor
ld. We lco me to fil e h and lin g i n G o. Error reading file: EOF
逐行讀取文件
本節我們討論如何使用 Go 逐行讀取文件。這可以使用 bufio 來實現。
請將 test.txt
替換為以下內容。
Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.
逐行讀取文件涉及到以下步驟。
- 打開文件;
- 在文件上新建一個 scanner;
- 掃描文件並且逐行讀取。
將 filehandling.go
替換為以下內容。
package main import ( "bufio" "flag" "fmt" "log" "os" ) func main() { fptr := flag.String("fpath", "test.txt", "file path to read from") flag.Parse() f, err := os.Open(*fptr) if err != nil { log.Fatal(err) } defer func() { if err = f.Close(); err != nil { log.Fatal(err) } }() s := bufio.NewScanner(f) for s.Scan() { fmt.Println(s.Text()) } err = s.Err() if err != nil { log.Fatal(err) } }
在上述程序的第 15 行,我們用命令行標記傳入的路徑,打開文件。在第 24 行,我們用文件創建了一個新的 scanner。第 25 行的 Scan()
方法讀取文件的下一行,如果可以讀取,就可以使用 Text()
方法。
當 Scan
返回 false 時,除非已經到達文件末尾(此時 Err()
返回 nil
),否則 Err()
就會返回掃描過程中出現的錯誤。
如果我使用下面命令來運行程序:
$ go install filehandling $ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt
程序會輸出:
Hello World. Welcome to file handling in Go. This is the second line of the file. We have reached the end of the file.
本教程到此結束。希望你能喜歡,祝你愉快。
https://studygolang.com/articles/14669