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.FS
、fs.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.Name
和 entry.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" ) 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" ) func main() { bucketURL := "gs://my-bucket" bucket, err := blob.OpenBucket(context.Background(), bucketURL) if err != nil { log.Fatal(err) } defer bucket.Close() fsys := blob.NewFS(bucket) 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
函数遍历存储桶中的文件。