摘要
go1.16新增了一个包,io/fs
,用来统一标准库文件io相关的访问方式。本文通过对比新旧文件io的用法差异,带着问题查看源码:标准库是如何保证兼容性的,如何实现一个基于操作系统中的FS,以及如何做单元测试。相信看完后大家会掌握go1.16之后文件操作的正确姿势。
背景
最近做的一个项目需要频繁和文件系统打交道,在本地写了不少单元测试。刚好近期go更新到了1.16,新增了io/fs
包,抽象了统一的文件访问相关接口,对测试也更友好,于是在项目中实践了一下,体验确实不错。
为了让大家更好的了解它,使用它,今天我们来从聊聊它的设计与实现。
下一篇会通过fs.FS
接口来设计和实现一个*对象存储FS*,敬请期待。
先来看看我们现在是怎么处理文件io的。
go 1.16 前后的文件io对比
// findExtFile 查找dir目录下的所有文件,返回第一个文件名以ext为扩展名的文件内容
//
// 假设一定存在至少一个这样的文件
func findExtFile(dir string, ext string) ([]byte, error) {
entries, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
for _, e := range entries {
if filepath.Ext(e.Name()) == ext && !e.IsDir() {
name := filepath.Join(dir, e.Name())
// 其实可以一行代码返回,这里只是为了展示更多的io操作
// return ioutil.ReadFile(name)
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
}
panic("never happen")
}
注释说得很清楚这函数是要干啥了,这里面有几个关键函数,ioutil.ReadDir
用于读取目录下的所有文件(非递归),os.Open
用于打开文件,最后ioutil.ReadAll
用于将整个文件内容读到内存中。
比较直接,我们来看看在go1.16及之后对应的函数实现:
func findExtFileGo116(dir string, ext string) ([]byte, error) {
fsys := os.DirFS(dir) // 以dir为根目录的文件系统,也就是说,后续所有的文件在这目录下
entries, err := fs.ReadDir(fsys, ".") // 读当前目录
if err != nil {
return nil, err
}
for _, e := range entries {
if filepath.Ext(e.Name()) == ext && !e.IsDir() {
// 也可以一行代码返回
// return fs.ReadFile(fsys, e.Name())
f, err := fsys.Open(e.Name()) // 文件名fullname `{dir}/{e.Name()}``
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
}
panic("never happen")
}
代码主体基本没变,最大的不同就是在文件io调用上,我们使用了一个os.DirFS
,这是go内建的基于目录树的os文件系统实现,接下来的几个操作都是基于这个fs。比如读取目录内容fs.ReadDir(fsys, '')
还有打开文件fsys.Open(file)
,实际上文件io都委托给这个fs了,添加了一层抽象。
注意到个别熟悉的老朋友
ioutil.ReadAll
变成了io.ReadAll
,这也是go1.16的一个变化,ioutil包被废弃了,对应的函数在io
和os
中。
让我们来看看这个dirFS
到底是什么,它的定义是这样的:
func DirFS(dir string) fs.FS {
return dirFS(dir)
}
type dirFS string
func (dir dirFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
}
f, err := Open(string(dir) + "/" + name)
if err != nil {
return nil, err // nil fs.File
}
return f, nil
}
os.DirFS
返回了一个fs.FS
类型的接口实现,这个接口只有一个方法Open
,实现上也很简单,直接调用os.Open
得到一个*os.File
对象,注意到返回类型是fs.File
,所以这意味着前者实现了后者。
还有一个关键点,打开的文件名是以dir为根路径的,完整的文件名是dir/name
,这点一定要注意,并且Open的name参数一定不能以/
打头或者结尾,为什么会这样呢?让我们走进go1.16的io/fs
包。
Go 1.16 fs 包
Go1.16新增了一个io/fs
包,定义了fs.FS
的接口,抽象了一个只读的文件树,标准库的包已经适配了这个接口。也是对新增的embed
包的抽象,所有的文件相关的都可以基于这个包来抽象,我觉得挺好的,方便了测试。不太好的地方就是*只读*的,无法写,当然这不妨碍我们去扩展接口。
同时,io/ioutil
这个包被废弃了,