趁着假期, 快速了解 Go io/fs 包

Go 语言的 io/fs 包是 Go 1.16 版本引入的一个标准库包,它定义了文件系统的抽象接口。这个包提供了一种统一的方式来访问不同类型的文件系统,包括本地文件系统、内存文件系统、zip 文件等。

io/fs 包的主要作用

  • 抽象文件系统: io/fs 包定义了一组接口,用于描述文件系统的基本操作,如打开文件、读取目录等。通过这些接口,我们可以编写与具体文件系统无关的代码。
  • 统一访问方式: 无论底层文件系统是什么类型,只要实现了 io/fs 包定义的接口,就可以使用相同的代码进行访问。
  • 提高代码可测试性: 通过使用 io/fs 包,我们可以方便地mock文件系统,从而提高代码的可测试性。

io/fs 包的核心接口

  • fs.FS 表示一个文件系统,定义了打开文件的方法 Open
  • fs.File 表示一个打开的文件,定义了读取、写入、关闭等方法。
  • fs.FileInfo 表示文件的元信息,包括文件名、大小、修改时间等。
  • fs.DirEntry 接口表示一个目录项,它可以是文件或子目录。
  • fs.FileInfo 接口表示文件的元信息。
  • fs.FileMode 类型表示文件的权限和类型,它是一个位掩码。

还有一些基于fs.FSfs.File等接口扩展的一些接口:

  • fs.GlobFS 接口扩展了 fs.FS 接口,增加了 Glob(pattern string) ([]string, error) 方法。该方法允许使用通配符模式匹配文件和目录。
  • fs.ReadDirFS 接口也扩展了 fs.FS 接口,增加了 ReadDir(name string) ([]fs.DirEntry, error) 方法。该方法用于读取指定目录下的所有文件和子目录。
  • fs.ReadDirFile 接口扩展了 fs.File 接口,增加了 ReadDir(n int) ([]fs.DirEntry, error) 方法。这个接口主要用于读取目录文件中的内容,返回一个 fs.DirEntry 列表。它通常用于实现了 fs.ReadDirFS 的文件系统。
  • fs.ReadFileFS 接口扩展了 fs.FS 接口,增加了 ReadFile(name string) ([]byte, error) 方法。这个接口允许直接读取指定文件的全部内容,返回字节切片。 它提供了一种更便捷的方式来读取文件内容,避免了先打开文件再读取的步骤。
  • fs.StatFS 接口也扩展了 fs.FS 接口,增加了 Stat(name string) (fs.FileInfo, error) 方法。该方法用于获取指定文件的元信息,返回一个 fs.FileInfo 对象。
  • fs.SubFS 接口也扩展了 fs.FS 接口,增加了 Sub(dir string) (fs.FS, error) 方法。该方法用于创建一个新的文件系统,它表示原始文件系统的一个子目录。这在需要限制访问文件系统的特定部分时非常有用。
  • fs.WalkDirFunc 类型定义了一个函数签名,用于 fs.WalkDir 函数的回调。

io/fs 包的应用场景

  • 访问不同类型的文件系统: 可以使用相同的代码访问本地文件系统、内存文件系统、zip 文件等。
  • 测试代码: 可以方便地mock文件系统,从而提高代码的可测试性。
  • 嵌入资源: 可以将静态资源嵌入到程序中,并使用 io/fs 包进行访问。

示例代码

示例代码一:fs.FS 接口

fs.FS 接口是 io/fs 包的核心,它表示一个文件系统。最常见的实现是 os.DirFS,它表示本地文件系统的一个目录。

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
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
// 创建一个表示当前目录的文件系统
fsys := os.DirFS(".")
// 打开一个文件
f, err := fsys.Open("README.md")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 读取文件内容
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data[:n]))
}

这个例子展示了如何使用 os.DirFS 创建一个文件系统,然后使用 fsys.Open 方法打开一个文件并读取其内容。

示例代码二:fs.File 接口

fs.File 接口表示一个打开的文件,它提供了读取、写入、关闭等方法。

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"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
f, err := fsys.Open("README.md")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 获取文件信息
info, err := f.Stat()
if err != nil {
log.Fatal(err)
}
fmt.Println("File size:", info.Size())
}

这个例子展示了如何使用 f.Stat 方法获取文件的元信息。

示例代码三:fs.DirEntry 接口

fs.DirEntry 接口表示一个目录项,它可以是文件或子目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println("Name:", entry.Name())
fmt.Println("Is directory:", entry.IsDir())
}
}

这个例子展示了如何使用 fs.ReadDir 函数读取目录中的所有条目,并使用 entry.Nameentry.IsDir 方法获取条目的名称和类型。

示例代码四:fs.GlobFS 接口

fs.GlobFS 接口扩展了 fs.FS 接口,增加了 Glob 方法,允许使用通配符模式匹配文件和目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
if globFS, ok := fsys.(fs.GlobFS); ok {
matches, err := globFS.Glob("*.go")
if err != nil {
log.Fatal(err)
}
fmt.Println("Go files:", matches)
}
}

这个例子展示了如何使用 fs.Glob 函数查找所有以 .go 结尾的文件。

示例代码五:fs.ReadDirFS 接口

fs.ReadDirFS 接口也扩展了 fs.FS 接口,增加了 ReadDir 方法,用于读取指定目录下的所有文件和子目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
if readDirFS, ok := fsys.(fs.ReadDirFS); ok {
entries, err := readDirFS.ReadDir(".")
if err != nil {
log.Fatal(err)
}
fmt.Println("Directory contents:")
for _, entry := range entries {
fmt.Println(entry.Name())
}
}
}

这个例子展示了如何使用 fs.ReadDir 函数读取目录中的所有条目。

示例代码六:fs.SubFS 接口

fs.SubFS 接口也扩展了 fs.FS 接口,增加了 Sub 方法,用于创建一个新的文件系统,它表示原始文件系统的一个子目录。

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
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
if subFS, ok := fsys.(fs.SubFS); ok {
sub, err := subFS.Sub("subdir")
if err != nil {
log.Fatal(err)
}
fmt.Println("Sub directory contents:")
entries, err := fs.ReadDir(sub, ".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println(entry.Name())
}
}
}

这个例子展示了如何使用 fs.Sub 函数创建一个表示子目录的文件系统,并读取其内容。

示例代码七:fs.WalkDirFunc 接口

fs.WalkDirFunc 类型定义了一个函数签名,用于 fs.WalkDir 函数的回调。

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
package main
import (
"fmt"
"io/fs"
"log"
"os"
)
func main() {
fsys := os.DirFS(".")
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println("Walking:", path)
return nil
})
if err != nil {
log.Fatal(err)
}
}

这个例子展示了如何使用 fs.WalkDir 函数遍历目录,并使用 fs.WalkDirFunc 函数打印每个文件和目录的路径。

那些有趣的文件系统

内存文件系统

内存文件系统是一种虚拟文件系统,它将文件存储在内存中而不是磁盘上。内存文件系统通常用于临时存储数据,或者用于测试和调试目的。
这种文件系统速度非常快,但数据在程序退出后会丢失。Go 语言的 testing/fstest 包提供了一个 MapFS 包,可以方便地创建内存文件系统。

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
41
42
43
44
45
46
package main
import (
"fmt"
"io/fs"
"log"
"os"
"testing/fstest"
)
func main() {
// 创建一个内存文件系统
fsys := fstest.MapFS{
"file1.txt": {Data: []byte("Hello, world!")},
"dir1/file2.txt": {Data: []byte("This is file2.")},
}
// 打开一个文件
f, err := fsys.Open("file1.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 读取文件内容
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data[:n]))
// 遍历文件系统
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println("Walking:", path)
return nil
})
if err != nil {
log.Fatal(err)
}
}

也有一些第三方的库实现了内存文件系统,比如psanford/memfs,这是一个它的例子:

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
package main
import (
"fmt"
"io/fs"
"github.com/psanford/memfs"
)
func main() {
rootFS := memfs.New()
err := rootFS.MkdirAll("dir1/dir2", 0777)
if err != nil {
panic(err)
}
err = rootFS.WriteFile("dir1/dir2/f1.txt", []byte("incinerating-unsubstantial"), 0755)
if err != nil {
panic(err)
}
err = fs.WalkDir(rootFS, ".", func(path string, d fs.DirEntry, err error) error {
fmt.Println(path)
return nil
})
if err != nil {
panic(err)
}
content, err := fs.ReadFile(rootFS, "dir1/dir2/f1.txt")
if err != nil {
panic(err)
}
fmt.Printf("%s\n", content)
}

嵌入式文件系统

嵌入式文件系统将文件嵌入到程序中,这样可以方便地将静态资源打包到程序中。Go 语言标准库提供了一个 embed 包,可以方便地创建嵌入式文件系统。

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
41
42
package main
import (
"embed"
"fmt"
"io/fs"
"log"
)
//go:embed static
var staticFiles embed.FS
func main() {
// 打开一个嵌入的文件
f, err := staticFiles.Open("static/file1.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 读取文件内容
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data[:n]))
// 遍历嵌入式文件系统
err = fs.WalkDir(staticFiles, "static", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println("Walking:", path)
return nil
})
if err != nil {
log.Fatal(err)
}
}

这个例子展示了如何使用embed.FS类型创建一个嵌入式文件系统,并使用staticFiles.Open` 方法打开一个嵌入的文件。

云存储文件系统

有一些第三方库提供了将 S3 存储桶挂载为本地文件系统的功能,这样我们就可以像访问本地文件一样访问 S3 文件。例如,go-cloud 库就提供了对多种云存储服务的统一访问接口,包括 S3。

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
package main
import (
"context"
"fmt"
"io/fs"
"log"
"gocloud.dev/blob"
_ "gocloud.dev/blob/gcs" // 引入 GCS 驱动,如果使用其他云存储服务,请引入相应的驱动
)
func main() {
// 设置 S3 存储桶 URL
bucketURL := "gs://my-bucket"
// 创建一个 blob.Bucket
bucket, err := blob.OpenBucket(context.Background(), bucketURL)
if err != nil {
log.Fatal(err)
}
defer bucket.Close()
// 创建一个 fs.FS
fsys := blob.NewFS(bucket)
// 现在可以使用 fsys 进行文件操作
err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println("Walking:", path)
return nil
})
if err != nil {
log.Fatal(err)
}
}

这个例子展示了如何使用 gocloud.dev/blob 包将 google GCS 存储桶挂载为本地文件系统,并使用 fs.WalkDir 函数遍历存储桶中的文件。