读取文件
Q7nl1s admin

文件读取是任何编程语言中最常见的操作之一。在本教程中,我们将学习如何使用Go来读取文件。

本教程有以下几个部分。

  • 将整个文件读入内存
    • 使用绝对文件路径
    • 将文件路径作为一个命令行flag来传递
    • 将文件捆绑在二进制文件中
  • 读取一个小块的文件
  • 逐行读取文件

将整个文件读入内存

最基本的文件操作之一是将整个文件读入内存。这是在os包的ReadFile函数的帮助下完成的。

让我们来读一个文件并打印其内容。

我通过运行mkdir ~/Documents/filehandling 在我的Documents目录下创建了一个文件夹filehandling

通过在filehandling目录下运行以下命令,创建一个名为filehandling的Go模块。

1
go mod init filehandling  

我有一个文本文件test.txt,它将从我们的Go程序filehandling.go中读取。test.txt包含以下字符串

1
Hello World. Welcome to file handling in Go.  

这是我的目录结构。

1
2
3
4
5
├── Documents
│ └── filehandling
│ ├── filehandling.go
| ├── go.mod
│ └── test.txt

让我们马上开始写代码。创建一个filehandling.go,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)

func main() {
contents, err := os.ReadFile("test.txt")
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(contents))
}

请在你的本地环境中运行这个程序,因为不可能在playground上读取文件。

上述程序的第9行读取文件并返回一个字节切片,该字节切片被存储在contents中。在第14行中,我们将contents转换为string,并将其储存在contents中。

请在test.txt所在的位置上运行这个程序。

如果test.txt位于~/Documents/filehandling,那么用以下步骤运行这个程序。

1
2
3
cd ~/Documents/filehandling/  
go install
filehandling

如果你不知道如何运行Go程序,请访问https://www.wuster.store/2022/09/15/Hello-World-0/,了解更多。如果你想了解更多关于包和Go模块的信息,请访问https://www.wuster.store/2022/09/21/%E5%8C%85/

这个程序会打印出来。

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

如果从其他位置运行此程序,例如,尝试从~/Documents/运行此程序。

1
2
cd ~/Documents/  
filehandling

它将打印出以下错误。

1
File reading error open test.txt: no such file or directory  

原因是Go是一种编译语言。go install所做的是,它从源代码中创建一个二进制文件。二进制文件独立于源代码,它可以从任何位置运行。由于在运行二进制文件的位置上找不到 test.txt,程序会抱怨说找不到指定的文件。

有三种方法来解决这个问题。

  1. 使用绝对文件路径
  2. 将文件路径作为一个命令行flag传递出去
  3. 将文本文件与二进制文件捆绑在一起

让我们来逐一讨论。

1. 使用绝对文件路径

解决这个问题的最简单方法是传递绝对文件路径。我已经修改了程序,并在第9行将路径改为绝对路径。9. 请把这个路径改为你的test.txt的绝对位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"os"
)

func main() {
contents, err := os.ReadFile("/Users/naveen/Documents/filehandling/test.txt")
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(contents))
}

现在,程序可以从任何地方运行,它将打印test.txt的内容。

例如,即使我从我的主目录中运行它,它也能工作

1
2
3
4
cd ~/Documents/filehandling  
go install
cd ~
filehandling

该程序将打印test.txt的内容。

这似乎是一个简单的方法,但也有一个缺陷,即文件应该位于程序中指定的路径中,否则这个方法会失败。

2. 将文件路径作为一个命令行flag传递

解决这个问题的另一个方法是将文件路径作为一个命令行参数来传递。使用flag包,我们可以从命令行获得文件路径作为输入参数,然后读取其内容。

让我们首先了解一下flag包是如何工作的。flag包有一个String函数。这个函数接受3个参数,第一个是flag的名称,第二个是默认值,第三个是对flag的简短描述。

让我们写一个小程序,从命令行读取文件名。用以下内容替换filehandling.go的内容。

1
2
3
4
5
6
7
8
9
10
11
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行,创建了一个名为fpath的字符串flag,默认值为test.txt,描述文件路径,使用String函数读取。这个函数返回存储flag值的字符串变量的地址。

flag.Parse()应该在访问任何flag之前被调用。

我们在第1行打印flag的值。

当这个程序使用命令运行时

1
filehandling -fpath=/path-of-file/test.txt  

我们将/path-of-file/test.txt作为flag的fpath值。

这个程序的输出结果是

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

如果程序运行时只使用文件处理而不传递任何fpath,它将打印出

1
value of fpath is test.txt  

因为test.txtfpath的默认值。

flag还提供了一个格式良好的不同参数的输出。这可以通过运行

1
filehandling --help  

这个命令将打印以下输出。

1
2
3
Usage of filehandling:  
-fpath string
file path to read from (default "test.txt")

不错吧:)。

现在我们知道如何从命令行读取文件路径,让我们继续完成我们的文件读取程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"flag"
"fmt"
"os"
)

func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
contents, err := os.ReadFile(*fptr)
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(contents))
}

上面的程序读取了从命令行传来的文件路径的内容。使用以下命令运行这个程序

1
filehandling -fpath=/path-of-file/test.txt  

请用test.txt的绝对路径替换/path-of-file/。例如,在我的例子中,我运行的命令是

1
filehandling --fpath=/Users/naveen/Documents/filehandling/test.txt  

和打印的程序。

1
Contents of file: Hello World. Welcome to file handling in Go.  
3. 将文本文件与二进制文件捆绑在一起

上述从命令行获取文件路径的方法很好,但还有一个更好的方法来解决这个问题。如果我们能够将文本文件与二进制文件捆绑在一起,那不是很好吗?这就是我们接下来要做的。

来自标准库的embed包将帮助我们实现这一目标。

在导入embed包后,可以使用//go:embed指令来读取文件的内容。

一个程序将使我们更好地理解事情。

用以下内容替换filehandling.go的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
_ "embed"
"fmt"
)

//go:embed test.txt
var contents []byte

func main() {
fmt.Println("Contents of file:", string(contents))
}

在上述程序的第4行,我们用下划线_前缀导入了embed包。原因是在代码中没有明确使用embed,但在第8行的//go:embed注释需要通过一些预处理。因为我们需要在没有明确使用的情况下导入包,所以我们用下划线_作为前缀来使编译器满意。否则,编译器会panic说这个包没有在任何地方被使用。

第8行中的//go:embed test.txt告诉编译器要读取test.txt的内容并将其分配给该注释后面的变量。在我们的例子中,contents变量将保存文件的内容。

使用以下命令运行该程序。

1
2
3
cd ~/Documents/filehandling  
go install
filehandling

并且该程序将打印

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

现在,该文件与二进制文件捆绑在一起,无论从哪里执行,它对go二进制文件都是可用的。例如,尝试从test.txt不在的目录中运行该程序。

1
2
cd ~/Documents  
filehandling

上述命令也将打印出该文件的内容。

请注意,分配给文件内容的变量必须是软件包级别的,本地变量不起作用。试着把程序改成下面的样子。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
_ "embed"
"fmt"
)

func main() {
//go:embed test.txt
var contents []byte
fmt.Println("Contents of file:", string(contents))
}

上述程序将内容作为一个局部变量。

现在该程序将无法编译,出现以下错误。

1
./filehandling.go:9:4: go:embed cannot apply to var inside func

如果你有兴趣了解更多关于这背后的设计决策,请阅读 https://github.com/golang/go/issues/43216

分块读取文件

在上一节中,我们学习了如何将整个文件加载到内存中。当文件的大小非常大时,将整个文件读入内存是没有意义的,特别是当你的内存不足时。一个更理想的方法是分块读取文件。这可以在bufio包的帮助下完成。

让我们写一个程序,将test.txt文件分成3个字节的块进行读取。将filehandling.go改为以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"bufio"
"flag"
"fmt"
"io"
"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 {
n, err := r.Read(b)
if err == io.EOF {
fmt.Println("finished reading file")
break
}
if err != nil {
fmt.Printf("Error %s reading file", err)
break
}
fmt.Println(string(b[0:n]))
}
}

在上述程序的第16行中,我们使用命令行flag传递的路径打开文件。

在第20行,我们defer了文件关闭操作。

上述程序的第26行创建了一个NewReader。在下一行,我们创建一个长度和容量为3的字节切片,文件的字节将被读入其中。

第29行的Read方法最多读取len(b)个字节,即最多3个字节,并返回读取的字节数。我们将返回的字节存储在一个变量中。在第38行中,我们将从一个变量中读取切片。从索引0n-1,即到Read方法返回的字节数为止,读取片断并打印。

一旦到达文件的末尾,read将返回一个EOF error。我们在第30行检查这个错误。该程序的其余部分是十分明确的。

如果我们用命令运行上面的程序。

1
2
3
cd ~/Documents/filehandling  
go install
filehandling -fpath=/path-of-file/test.txt

将会输出以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hel  
lo
Wor
ld.
We
lco
me
to
fil
e h
and
lin
g i
n G
o.
finished reading file

逐行读取文件

在本节中,我们将讨论如何使用Go来逐行读取文件。这可以用bufio包来完成。

请将test.txt中的内容替换为以下内容

1
2
3
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中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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行中,我们使用命令行flag传递的路径打开文件。在第24行,我们使用该文件创建一个NewScanner。第25行的scan()方法读取文件的下一行,读取的字符串将通过Text()方法获得。

Scan返回false后,Err()方法将返回扫描过程中发生的任何错误。如果错误是End of FileErr()将返回nil

如果我们使用命令运行上面的程序。

1
2
3
cd ~/Documents/filehandling  
go install
filehandling -fpath=/path-of-file/test.txt

文件的内容将被逐行打印出来,如下图所示。

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

这样我们就到了本节的结尾。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View