1. 程式人生 > 其它 >Go 讀取檔案

Go 讀取檔案

35. 讀取檔案

檔案讀取是所有程式語言中最常見的操作之一。本教程我們會學習如何使用 Go 讀取檔案。

本教程分為如下小節。

  • 將整個檔案讀取到記憶體
    • 使用絕對檔案路徑
    • 使用命令列標記來傳遞檔案路徑
    • 將檔案繫結在二進位制檔案中
  • 分塊讀取檔案
  • 逐行讀取檔案

將整個檔案讀取到記憶體

將整個檔案讀取到記憶體是最基本的檔案操作之一。這需要使用 ioutil]包中的 ReadFile 函式。

讓我們在 Go 程式所在的目錄中,讀取一個檔案。我已經在 GOPATH(譯註:原文是 GOROOT,應該是筆誤)中建立了資料夾,在該資料夾內部,有一個文字檔案 test.txt,我們會使用 Go 程式 filehandling.go

來讀取它。test.txt 包含文字 “Hello World. Welcome to file handling in Go”。我的資料夾結構如下:

Copy
src
    filehandling
        filehandling.go
        test.txt

接下來我們來看看程式碼。

Copy
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,可以使用下列步驟來執行程式。

Copy
$ cd /home/naveen/go/src/filehandling/
$ go install filehandling
$ workspacepath/bin/filehandling

對於 windows,如果 test.txt 位於 C:\Users\naveen.r\go\src\filehandling,則使用下列步驟。

Copy
> cd C:\Users\naveen.r\go\src\filehandling
> go install filehandling
> workspacepath\bin\filehandling.exe

該程式會輸出:

Copy
Contents of file: Hello World. Welcome to file handling in Go.

如果在其他位置執行這個程式(比如 /home/userdirectory),會列印下面的錯誤。

Copy
File reading error open test.txt: The system cannot find the file specified.

這是因為 Go 是編譯型語言。go install 會根據原始碼建立一個二進位制檔案。二進位制檔案獨立於原始碼,可以在任何位置上執行。由於在執行二進位制檔案的位置上沒有找到 test.txt,因此程式會報錯,提示無法找到指定的檔案。

有三種方法可以解決這個問題。

  1. 使用絕對檔案路徑
  2. 使用命令列標記來傳遞檔案路徑
  3. 將檔案繫結在二進位制檔案中

讓我們來依次介紹。

1. 使用絕對檔案路徑

要解決問題,最簡單的方法就是傳入絕對檔案路徑。我已經修改了程式,把路徑改成了絕對路徑。

Copy
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 的內容。

例如,可以在我的家目錄執行。

Copy
$ cd $HOME
$ go install filehandling
$ workspacepath/bin/filehandling

該程式打印出了 test.txt 的內容。

看似這是一個簡單的方法,但它的缺點是:檔案必須放在程式指定的路徑中,否則就會出錯。

2. 使用命令列標記來傳遞檔案路徑

另一種解決方案是使用命令列標記來傳遞檔案路徑。使用 flag 包,我們可以從輸入的命令列獲取到檔案路徑,接著讀取檔案內容。

首先我們來看看 flag 包是如何工作的。flag 包有一個名為 String函式。該函式接收三個引數。第一個引數是標記名,第二個是預設值,第三個是標記的簡短描述。

讓我們來編寫程式,從命令列讀取檔名。將 filehandling.go 的內容替換如下:

Copy
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 值。

使用下面命令執行程式。

Copy
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

我們傳入 /path-of-file/test.txt,賦值給了 fpath 標記。

該程式輸出:

Copy
value of fpath is /path-of-file/test.txt

這是因為 fpath 的預設值是 test.txt

現在我們知道如何從命令列讀取檔案路徑了,讓我們繼續完成我們的檔案讀取程式。

Copy
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))
}

在上述程式裡,命令列傳入檔案路徑,程式讀取了該檔案的內容。使用下面命令執行該程式。

Copy
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

請將 /path-of-file/ 替換為 test.txt 的真實路徑。該程式將列印:

Copy
Contents of file: Hello World. Welcome to file handling in Go.

3. 將檔案繫結在二進位制檔案中

雖然從命令列獲取檔案路徑的方法很好,但還有一種更好的解決方法。如果我們能夠將文字檔案捆綁在二進位制檔案,豈不是很棒?這就是我們下面要做的事情。

有很多可以幫助我們實現。我們會使用 packr,因為它很簡單,並且我在專案中使用它時,沒有出現任何問題。

第一步就是安裝 packr 包。

在命令提示符中輸入下面命令,安裝 packr 包。

Copy
go get -u github.com/gobuffalo/packr/...

packr 會把靜態檔案(例如 .txt 檔案)轉換為 .go 檔案,接下來,.go 檔案會直接嵌入到二進位制檔案中。packer 非常智慧,在開發過程中,可以從磁碟而非二進位制檔案中獲取靜態檔案。在開發過程中,當僅僅靜態檔案變化時,可以不必重新編譯。

我們通過程式來更好地理解它。用以下內容來替換 handling.go 檔案。

Copy
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 非常智慧,在開發階段可以從磁碟載入檔案。

使用下面命令來執行程式。

Copy
go install filehandling
workspacepath/bin/filehandling

該命令可以在其他位置執行。packr 很聰明,可以獲取傳遞給 NewBox 命令的目錄的絕對路徑。

該程式會輸出:

Copy
Contents of file: Hello World. Welcome to file handling in Go.

你可以試著改變 test.txt 的內容,然後再執行 filehandling。可以看到,無需再次編譯,程式打印出了 test.txt 的更新內容。完美!

現在我們來看看如何將 test.txt 打包到我們的二進位制檔案中。我們使用 packr 命令來實現。

執行下面的命令:

Copy
packr install -v filehandling

它會列印:

Copy
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 的內容。

Copy
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 錯誤。程式的其他地方比較簡單,不做解釋。

如果我們使用下面命令來執行程式:

Copy
$ go install filehandling
$ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt

會得到以下輸出:

Copy
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 替換為以下內容。

Copy
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 替換為以下內容。

Copy
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() 就會返回掃描過程中出現的錯誤。

如果我使用下面命令來執行程式:

Copy
$ go install filehandling
$ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt

程式會輸出:

Copy
Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.