![reading files](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/golang-read-files.png)歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 35 篇。檔案讀取是所有程式設計語言中最常見的操作之一。本教程我們會學習如何使用 Go 讀取檔案。本教程分為如下小節。- 將整個檔案讀取到記憶體 - 使用絕對檔案路徑 - 使用命令列標記來傳遞檔案路徑 - 將檔案綁定在二進位檔案中- 分塊讀取檔案- 逐行讀取檔案## 將整個檔案讀取到記憶體將整個檔案讀取到記憶體是最基本的檔案操作之一。這需要使用 [`ioutil`](https://golang.org/pkg/io/ioutil/) 包中的 [`ReadFile`](https://golang.org/pkg/io/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```接下來我們來看看代碼。```gopackage mainimport ( "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 行,程式會讀取檔案,並返回一個位元組[切片](https://studygolang.com/articles/12121),而這個切片儲存在 `data` 中。在第 14 行,我們將 `data` 轉換為 `string`,顯示出檔案的內容。請在 **test.txt** 所在的位置運行該程式。例如,對於 **linux/mac**,如果 **test.txt** 位於 **/home/naveen/go/src/filehandling**,可以使用下列步驟來運行程式。```bash$ cd /home/naveen/go/src/filehandling/$ go install filehandling$ workspacepath/bin/filehandling```對於 **windows**,如果 **test.txt** 位於 **C:\Users\naveen.r\go\src\filehandling**,則使用下列步驟。```bash> cd C:\Users\naveen.r\go\src\filehandling> go install filehandling> workspacepath\bin\filehandling.exe```該程式會輸出:```basContents of file: Hello World. Welcome to file handling in Go.```如果在其他位置運行這個程式(比如 `/home/userdirectory`),會列印下面的錯誤。```bashFile reading error open test.txt: The system cannot find the file specified.```這是因為 Go 是編譯型語言。`go install` 會根據原始碼建立一個二進位檔案。二進位檔案獨立於原始碼,可以在任何位置上運行。由於在運行二進位檔案的位置上沒有找到 `test.txt`,因此程式會報錯,提示無法找到指定的檔案。有三種方法可以解決這個問題。1. 使用絕對檔案路徑2. 使用命令列標記來傳遞檔案路徑3. 將檔案綁定在二進位檔案中讓我們來依次介紹。### 1. 使用絕對檔案路徑要解決問題,最簡單的方法就是傳入絕對檔案路徑。我已經修改了程式,把路徑改成了絕對路徑。```gopackage mainimport ( "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` 的內容。例如,可以在我的家目錄運行。```bash$ cd $HOME$ go install filehandling$ workspacepath/bin/filehandling```該程式列印出了 `test.txt` 的內容。看似這是一個簡單的方法,但它的缺點是:檔案必須放在程式指定的路徑中,否則就會出錯。### 2. 使用命令列標記來傳遞檔案路徑另一種解決方案是使用命令列標記來傳遞檔案路徑。使用 [flag](https://golang.org/pkg/flag/) 包,我們可以從輸入的命令列擷取到檔案路徑,接著讀取檔案內容。首先我們來看看 `flag` 包是如何工作的。`flag` 包有一個名為 [`String`](https://golang.org/pkg/flag/#String) 的[函數](https://studygolang.com/articles/11892)。該函數接收三個參數。第一個參數是標記名,第二個是預設值,第三個是標記的簡短描述。讓我們來編寫程式,從命令列讀取檔案名稱。將 `filehandling.go` 的內容替換如下:```gopackage mainimport ( "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 值的字串[變數](https://studygolang.com/articles/11756)的地址。在程式訪問 flag 之前,必須先調用 `flag.Parse()`。在第 10 行,程式會列印出 flag 值。使用下面命令運行程式。```bashwrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt```我們傳入 `/path-of-file/test.txt`,賦值給了 `fpath` 標記。該程式輸出:```bashvalue of fpath is /path-of-file/test.txt```這是因為 `fpath` 的預設值是 `test.txt`。現在我們知道如何從命令列讀取檔案路徑了,讓我們繼續完成我們的檔案讀取程式。```gopackage mainimport ( "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))}```在上述程式裡,命令列傳入檔案路徑,程式讀取了該檔案的內容。使用下面命令運行該程式。```bashwrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt```請將 `/path-of-file/` 替換為 `test.txt` 的真實路徑。該程式將列印:```txtContents of file: Hello World. Welcome to file handling in Go.```### 3. 將檔案綁定在二進位檔案中雖然從命令列擷取檔案路徑的方法很好,但還有一種更好的解決方案。如果我們能夠將文字檔捆綁在二進位檔案,豈不是很棒?這就是我們下面要做的事情。有很多[包](https://studygolang.com/articles/11893)可以協助我們實現。我們會使用 [packr](https://github.com/gobuffalo/packr),因為它很簡單,並且我在項目中使用它時,沒有出現任何問題。第一步就是安裝 `packr` 包。在命令提示字元中輸入下面命令,安裝 `packr` 包。```bashgo get -u github.com/gobuffalo/packr/...````packr` 會把靜態檔案(例如 `.txt` 檔案)轉換為 `.go` 檔案,接下來,`.go` 檔案會直接嵌入到二進位檔案中。`packer` 非常智能,在開發過程中,可以從磁碟而非二進位檔案中擷取靜態檔案。在開發過程中,當僅僅靜態檔案變化時,可以不必重新編譯。我們通過程式來更好地理解它。用以下內容來替換 `handling.go` 檔案。```gopackage mainimport ( "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` 非常智能,在開發階段可以從磁碟負載檔案。使用下面命令來運行程式。```bashgo install filehandlingworkspacepath/bin/filehandling```該命令可以在其他位置運行。`packr` 很聰明,可以擷取傳遞給 `NewBox` 命令的目錄的絕對路徑。該程式會輸出:```txtContents of file: Hello World. Welcome to file handling in Go.```你可以試著改變 `test.txt` 的內容,然後再運行 `filehandling`。可以看到,無需再次編譯,程式列印出了 `test.txt` 的更新內容。完美!:)現在我們來看看如何將 `test.txt` 打包到我們的二進位檔案中。我們使用 `packr` 命令來實現。運行下面的命令:```bashpackr install -v filehandling```它會列印:```bashbuilding box ../filehandlingpacking file filehandling.gopacked file filehandling.gopacking file test.txtpacked file test.txtbuilt box ../filehandling with ["filehandling.go" "test.txt"]filehandling```該命令將靜態檔案綁定到了二進位檔案中。在運行上述命令之後,使用命令 `workspacepath/bin/filehandling` 來運行程式。程式會列印出 `test.txt` 的內容。於是從二進位檔案中,我們讀取了 `test.txt` 的內容。如果你不知道檔案到底是由二進位還是磁碟來提供,我建議你刪除 `test.txt`,並在此運行 `filehandling` 命令。你將看到,程式列印出了 `test.txt` 的內容。太棒了:D。我們已經成功將靜態檔案嵌入到了二進位檔案中。## 分塊讀取檔案在前面的章節,我們學習了如何把整個檔案讀取到記憶體。當檔案非常大時,尤其在 RAM 儲存量不足的情況下,把整個檔案都讀入記憶體是沒有意義的。更好的方法是分塊讀取檔案。這可以使用 [bufio](https://golang.org/pkg/bufio) 包來完成。讓我們來編寫一個程式,以 3 個位元組的塊為單位讀取 `test.txt` 檔案。如下所示,替換 `filehandling.go` 的內容。```gopackage mainimport ( "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` [方法](https://studygolang.com/articles/12264)會讀取 len(b) 個位元組(達到 3 位元組),並返回所讀取的位元組數。當到達檔案最後時,它會返回一個 EOF 錯誤。程式的其他地方比較簡單,不做解釋。如果我們使用下面命令來運行程式:```bash$ go install filehandling$ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt```會得到以下輸出:```bashHelloWorld. Welcometofile handling in Go.Error reading file: EOF```## 逐行讀取檔案本節我們討論如何使用 Go 逐行讀取檔案。這可以使用 [bufio](https://golang.org/pkg/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.```逐行讀取檔案涉及到以下步驟。1. 開啟檔案;2. 在檔案上建立一個 scanner;3. 掃描檔案並且逐行讀取。將 `filehandling.go` 替換為以下內容。```gopackage mainimport ( "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()` 就會返回掃描過程中出現的錯誤。如果我使用下面命令來運行程式:```bash$ go install filehandling$ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt```程式會輸出:```bashHello 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/13178)
via: https://golangbot.com/read-files/
作者:Naveen Ramanathan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
430 次點擊