?
什么神奇问题 ?
?
Go 在文件 IO 的场景有个神奇的事情。打开一个文件的时候,返回的竟然不是 interface ,而是一个 os.File ?结构体的指针。
?
func?Open(name?string)?(*File,?error)?{ ????return?OpenFile(name,?O_RDONLY,?0) }
?
划重点:这个意味着,Go 的文件系统的概念和 OS 的文件系统的概念直接关联起来。你必须传入一个文件路径,并且必须真的要去打开一个操作系统的文件。
不用接口,而是跟具体类型强相关的话,会导致后续的扩展性不好。比如,全都是 os 包的使用,那么将操作强绑定在 OS 文件系统上。
最常见的,在单测的时候用的这种方式的话,就真的要在操作系统上打开文件做操作。Go 的设计者对此一直耿耿于怀,但是也很无奈。因为用户已经用上了,Go 的承诺是往前兼容,直接修改原有语义和接口肯定不行。
怎么办?
Go 1.16 给了我们答案。Go 给了我们一个 io.FS 的封装。Go 的意图是在自己的语言层面再做一层 FS 的抽象,这样就能和 OS 的 FS 解耦开来。io.FS 可以是任何奇形怪状的 FS ,只要你实现了规定好的 FS 接口。下一步来看下 Go 1.16 带来的几个核心改动。
有人说 Go 都 1.19 了,还看 1.16 ?
因为 Go 的 io/fs 是在 Go 1.16 引入的。在 io 方面有比较大的一个变化。
?
Go 1.16 关于 io 有哪些改变 ?
?
新增了一个 io/fs 的包,抽象了一个 FS 出来。
embed 的 package 用了这个抽象。
规整 io/ioutil 里面的内容。
接下来我们一个个看下。
?
io.FS 的抽象
?
?1???Go 为什么要抽象 FS ?
前面已经提到,Go 的文件系统的概念和 OS 的文件系统的概念直接关联起来。这个给扩展性带来了不方便。最重要的,Go 已经发现有和 OS 不同的文件系统的需求了,就是 embed FS 。
embed 是 Go 提供的一个打包文件到二进制的功能,也是类似文件系统的一种需求。但是却不是直接位于 OS 上的文件系统(vfs 那套东西)。
所以在 Go 1.16 顺势就一起上了。引入了 io.FS 的定义,并且 embed 就直接用上了这层抽象。
![[fs 封装层次.png]]
?2???来看下 FS 接口的定义
Go 的实现者们很强,推荐的是小接口。也就是最小化、原子化的接口语义。从 io/fs 的定义就能看到很强的功力。
?
//?文件系统的接口 type?FS?interface?{ ????Open(name?string)?(File,?error) } //?文件的接口 type?File?interface?{ ????Stat()?(FileInfo,?error) ????Read([]byte)?(int,?error) ????Close()?error }
?
这,就是最简单的 FS 。 这个就是文件系统极简的样子,只需要有一个 Open 方法,返回一个文件即可。
也就是说,Go 理解的文件系统,只要能实现一个 Open 方法,返回一个 File 的 interface ,这个 File 只需要实现 Stat,Read,Close 方法即可。
有没有发现,OS 的 FS 已经满足了条件。所以,Go 的 FS 可以是 OS 的 FS ,自然也可以是其他的实现。
Go 在此 io.FS 的基础上,再去扩展接口,增加文件系统的功能。比如,加个 ReadDir 就是一个有读目录的文件系统 ReadDirFS :
?
type?ReadDirFS?interface?{ ????FS ????//?读目录 ????ReadDir(name?string)?([]DirEntry,?error) }
?
加个 Glob 方法,就成为一个具备路径通配符查询的文件系统:
?
type?GlobFS?interface?{ ????FS ????//?路径通配符的功能 ????Glob(pattern?string)?([]string,?error) }
?
加个 Stat ,就变成一个路径查询的文件系统:
?
type?StatFS?interface?{ ????FS ????//?查询某个路径的文件信息 ????Stat(name?string)?(FileInfo,?error) }
?
这些非常经典的文件系统的定义 Go 在 io/fs 里面已经做好了。
?3???io.FS 怎么使用呢?
我们的目标是实现一个 Go 的 FS ,这个定义已经在 io.FS 有了。我们只需要写一个结构体,实现它的方法,那么你就可以说这是一个 FS 了。
这里其实就可以有非常多的想象空间,比如,可以是 OS 的 FS,也可以是 memory FS ,hash FS 等等。网上有不少例子。但其实标准库已经有一个最好的例子,那就是 embed FS 。
我们来看下 embed 怎么实现一个内嵌的文件系统。embed 的实现在 embed/embed.go 这个文件中,非常精简。
首先,在 embed package 里定义了一个结构体 FS ,这个结构体将是 io.FS 的具体实现。
?
//?作为具体?FS?的实现 type?FS?struct?{ ????files?*[]file } //?代表一个内嵌文件 type?file?struct?{ ????name?string ????data?string??//?文件的数据全在内存里 ????hash?[16]byte?//?truncated?SHA256?hash }
?
embed 里面的 FS 结构体只需要实现 Open 这个方法即可:
?
//?Open?的具体实现 func?(f?FS)?Open(name?string)?(fs.File,?error)?{ ????//?通过名字匹配查找到?file?对象 ????file?:=?f.lookup(name) ????//?如果没找到 ????if?file?==?nil?{ ????????return?nil,?&fs.PathError{Op:?"open",?Path:?name,?Err:?fs.ErrNotExist} ????} ????//?如果是目录结构 ????if?file.IsDir()?{ ????????return?&openDir{file,?f.readDir(name),?0},?nil ????} ????//?找到了就封装成?openFile?结构体 ????return?&openFile{file,?0},?nil }
?
上面的 Open ,如果是文件的化,返回的是一个 openFile 的结构体 ,作为 io.File 接口的具体实现:
?
//?代表一个文件的实现 type?openFile?struct?{ ????f?*file?//?the?file?itself ????offset?int64?//?current?read?offset } func?(f?*openFile)?Close()?error???????????????{?return?nil?} func?(f?*openFile)?Stat()?(fs.FileInfo,?error)?{?return?f.f,?nil?} func?(f?*openFile)?Read(b?[]byte)?(int,?error)?{ ????//?判断偏移是否符合预期 ????if?f.offset?>=?int64(len(f.f.data))?{ ????????return?0,?io.EOF ????} ????if?f.offset?0?{ ????????return?0,?&fs.PathError{Op:?"read",?Path:?f.f.name,?Err:?fs.ErrInvalid} ????} ????//?从内存拷贝数据 ????n?:=?copy(b,?f.f.data[f.offset:]) ????f.offset?+=?int64(n) ????return?n,?nil }
?
如上,只需要实现 Read,Stat,Close 方法即可。这就是一个完整的、Go 层面的 FS 的实现。
你可以如下使用 embed 文件系统:
?
//go:embed?hello.txt var?f?embed.FS func?main()?{ ????//?打开文件 ????file,?err?:=?f.Open("hello.txt") ????//?... ????//?读文件 ????n,?err?=?file.Read(/*buffer*/) }
?
上面的例子,编译的时候会把当前目录下的一个 hello.txt 文件打包到二进制文件。程序启动的时候可以把它读出来。
注意:f 这个变量,编译器会安排填充好。进程启动时它是有值的。
?
Go 1.16 关于 IO 其他的改动
?
除了上面提到的 io/fs 和 embed fs ,Go 对之前的 io 的一些结构也做了更准确的调整分类。把之前大杂烩的 io/ioutil 里面的东西拆出来了。移到对应的 io 包和 os 包。为了兼容性,ioutil 包并没有直接删除,而是导入。比如:
Discard 移到了 io 库实现
ReadAll 移到了 io 库实现
NopCloser 移到了 io 库实现
ReadFile 移到 os 库实现
WriteFile 移到 os 库实现
基本上 ioutil 这个 package 是被掏空了。Go 1.16 只是为了兼容性还没删。
?
Go 的 FS 封装有啥用呢 ?
?
好处其实很多,最明显的两个:
单测方便了。
有类似 embed FS 这种非 OS 文件系统的需求,可以有方法扩展了。
?
总结
?
Go 在自己的层面封装出一个 io.FS 的抽象,意图和 OS 的 FS 解耦。这样可以给程序员带来更多的想象空间 ;
embed FS 具备典型的 FS 的界面,但是它并不是直接位于 OS 的文件系统。所以它非常适合作为首个用 io.FS 的实践;
以后尽量用 io.FS 来管理的文件,这样可以做到和 OS 解耦,方便做单测;
ioutil 可以少用,它的功能已经被移到更明确的 package 里实现了;
审核编辑:汤梓红
评论