[Go 入门] 第二十章 处理文件

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

本文内容:

文件的重要性

文件能够让程序员管理配置、储存程序的状态乃至从底层操作系统中读取数据

UNIX 型操作系统 (Linux 和 macOS)的一个重要特征就是,将一切视为文件。这意味着操作系统看来,从键盘到打印机的所有东西都可像文件那样编址。在这方面,UNIX 走的更远,它通过虚拟文件系统来暴露系统信息。

在 UNIX 系统中,可使用命令 cat 来读取文件的内容并将其打印到终端。/proc/loadavg 就是一个有关系统当前负载的虚拟文件

script
1
2
cat /proc/loadavg
0.00 0.01 0.05 1/507 11105

再次执行,会发现输出的数值发生了变化,代表了有关系统负载的数据是实时的

script
1
2
cat /proc/loadavg
0.00 0.01 0.05 1/507 11115

在大多数 UNIX 系统中,可试用命令 watch 来创建有关系统负载的实时视图,这个视图每隔 2s 更新一次

script
1
2
3
4
5
watch cat /proc/loadavg

Every 2.0s: cat /proc/loadavg Mon Feb 3 05:59:31 2020

0.00 0.01 0.05 1/509 11195

使用 ioutil 包读写文件

鉴于处理文件是一种常见的任务,标准库提供了 ioutil 包,可执行包含以下操作:

  • 读取文件
  • 列出目录的内容
  • 创建临时目录
  • 创建临时文件
  • 创建文件
  • 写入文件

读取文件

ioutil 包提供了函数 Readfile ,可使用它来完成这项任务,这个函数将一个文件路径作为参数,并以字节切片的方式返回文件的内容

1
2
3
4
5
6
7
8
9
func main() {
fileBytes, err := ioutil.ReadFile("test.log")
if err != nil{
log.Fatal(err)
}
fmt.Println(fileBytes)
fileString := string(fileBytes)
fmt.Println(fileString)
}

创建文件

ioutil 包还提供了函数 WriteFile。这个函数用于将数据写入文件,但也可使用它来创建文件。接收一个文件路径,要写入的数据,以及应用于文件的权限

文件权限是由 UNIX 权限衍生而来的,它们对应3类用户:文件所有者、与文件位于同一组的用户、其他用户

Go 语言使用 UNIX 权限的数字表示法,很多用于处理文件的函数都将权限值作为参数

文件权限

符号表示法 数字表示法 说明
————— 0000 无权限
-rwx——— 0700 只有所有者能够读取、写入和执行
-rwxrwx—- 0770 所有者及其所在的用户组能够读取、写入和执行
-rwxrwxrwx 0777 所有人能够读取、写入和执行
—-x—x—x 0111 所有人都能够执行
—w—w—w- 0222 所有人都能够写入
—wx-wx-wx 0333 所有人都能够写入和执行
-r—r—r— 0444 所有人都能够读取
-r-xr-xr-x 0555 所有人都能都读取和执行
-rw-rw-rw- 0666 所有人都能够读取和写入
-rwxr——- 0740 所有者能够读取、写入和执行,而所有者所在的用户组能够读取

符号表示法是数字表示法的视觉表示:符号表示法总共包含10个字符。最左边的字符指出了文件是普通文件、目录还是其他东西,如果这个字符为-,就表示文件为普通文件,接下来的3个字符指定了文件所有者的权限;在接下来的3个字符表示所有者所在的用户组的权限,最后3个字符表示其他人的权限

在 UNIX 系统中,文件的默认权限为 0644 ,即所有者能够读取和写入。其他人只能够读取。

1
2
3
4
5
6
7
func main() {
b := make([]byte, 0)
err := ioutil.WriteFile("test.txt", b, 0644)
if err != nil{
log.Fatal(err)
}
}

在当前的目录下创建了一个文件 test.txt,并且文件权限为 0644

写入文件

使用 WriteFile 也可用来写入文件,如果指定的文件不存在,将会创建它

1
2
3
4
5
6
func main() {
err := ioutil.WriteFile("test.txt", []byte("Hello world!"), 0644)
if err != nil{
log.Fatal(err)
}
}

列出目录的内容

要处理文件系统中的文件,必须知道目录结构。为此,ioutil 包提供了便利函数 ReadDir, 它接收以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件。文件名的类型为 FileInfo,包含如下信息:

  • Name: 文件的名称
  • Size: 文件的长度,单位为字节
  • Mode: 权限
  • ModTime: 最后一次修改的时间
  • IsDir: 文件是否为目录
  • Sys: 底层数据源
1
2
3
4
5
6
7
8
9
10
func main() {
files, err := ioutil.ReadDir(".")
if err != nil{
log.Fatal(err)
}

for _, file := range files {
fmt.Println(file.Mode(), file.Name())
}
}

执行上面将会看到下面的类似输出:

1
2
3
4
-rw-rw-rw- dirname.go
-rw-rw-rw- go.mod
-rw-rw-rw- test.log
-rw-rw-rw- test.txt

复制文件

ioutil 包可用于执行一些常见的文件操作,但要执行更复杂的操作,使用 os 包,os 包运行在稍低的层级,因此使用它时,必须手工关闭打开的文件。ioutil包的很多函数都是os包包装器

要复制文件,只需结合使用 os 包中的几个函数。以编程复制文件的步骤如下:

  1. 打开要复制的文件
  2. 读取其内容
  3. 创建并打开要将这些内容复制到其中的文件
  4. 将内容写入这个文件
  5. 关闭所有已打开的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
from, err := os.Open("test.log")
if err != nil {
log.Fatal(err)
}
defer from.Close()

to, err := os.OpenFile("test.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
defer to.Close()

_, err = io.Copy(to, from)
if err !=nil{
log.Fatal(err)
}
}

删除文件

使用 os 包中的函数 Remove 即可删除文件,这个函数不会发出警告,也无法将删除的文件恢复,因此要务必谨慎

1
2
3
4
5
6
func main() {
err := os.Remove("test.txt")
if err != nil{
log.Fatal(err)
}
}

使用文件来管理配置

在编程中常使用文件来管理配置。考虑到代码可能在不同的环境中执行,可使用一个文件来设置各种用于启动程序的配置参数。使用文件是管理环境差异的有效方式,这种环境差别包括如下几个方面:

  • Web 服务的 URL
  • 访问密匙
  • 端口号
  • 环境变量

使用 JSON 文件

在声明可储存在文件中并在必要时读取的配置方面,JSON是一种卓有成效的标准方式。将配置储存在文件中的另一个优点是,可对其进行版本控制并集成到自动构建过程中。

通过将配置储存在 JSON 文件中,Go 程序中可读取这个文件,并将其作为配置数据使用

config.json

1
{"name":  "dirname", "blog":  "https://blog.forgiveher.cn"}
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
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
)

type Config struct {
Name string `json:"name"`
Blog string `json:"blog"`
}

func main() {
f, err := ioutil.ReadFile("config.json")
if err != nil{
log.Fatal(err)
}
c := Config{}
err = json.Unmarshal(f, &c)
if err != nil{
log.Fatal(err)
}
fmt.Printf("%+v", c)
}

使用 TOML 文件

TOML 是一种专为存储配置文件而设计的格式,它在 Go 社区中深受欢迎。相比于 JSON,其表达能力更强,且容易映射 Go 类型, JSON 是为序列化数据而设计的,而 TOML 是专门为储存配置文件而设计的,因此 TOML 相比于 JSON 有一定的优势,如更容易阅读、具备 JSON 没有的特性(如注释),在基本层面,TOML 的语法非常简单,可像 JSON 那样指定键和值

1
2
Name = "dirname"
Blog = "https://blog.forgiveher.cn"

config.toml

1
2
3
4
5
Name = "dirname"
Blog = "https://blog.forgiveher.cn"

[test]
Func = "hello()"
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
package main

import (
"fmt"
"github.com/BurntSushi/toml"
"log"
)

type Test struct {
Func string
}

type Config struct {
Name string
Blog string
Test Test
}

func main() {
c := Config{}
_, err := toml.DecodeFile("config.toml", &c)
if err != nil{
log.Fatal(err)
}
fmt.Printf("%+v", c)
}

执行后将输出:

1
{Name:dirname Blog:https://blog.forgiveher.cn Test:{Func:hello()}}

问题列表

  • ioutil 包为何没有提供用于执行复制文件等操作的函数

    Go 语言致力于确保核心库为小巧而轻量级的。另外,不同操作系统差别很大,这导致创建通用的问价你复制方法是很难的。

  • 操作文件前,Go 会不会核实文件确实存在

    不会,程序员必须在使用文件前核实它确实存在