Go语言在其设计中提供了一个独特的“文件系统”接口,这是由语言层面做的抽象,与操作系统的文件系统并不等价。这样设计的初衷在于解耦Go语言中文件处理与操作系统之间的直接依赖。
深入研究Go的文件操作机制,我们可以发现,文件操作并未以接口形式定义。相反,它直接与os.File结构体相关联。例如,os.Open函数用于打开文件,它返回一个os.File结构体的指针,而后续的文件I/O操作都是通过这个os.File结构体实现来进行的。os.Open函数的定义如代码清单3-19所示。
代码清单3-19 os.Open函数的定义
Open返回的是一个具体的类型,而非接口类型。这个选择令Go的开发者颇感困惑,因为这会导致业务代码和os.File类型紧密耦合。例如,业务函数执行了文件I/O操作,那么在单元测试时就必须真实地创建一个文件。如果os.Open返回的是接口类型,那么在测试时可以传入满足该接口的任何类型,大大简化了测试过程。
Go在1.16版本中引入io.FS接口,以应对与操作系统的文件系统不同的I/O需求。例如,embed文件系统——一种将文件嵌入二进制程序的机制。通过在io/fs为文件系统提供抽象定义,使Go中的文件系统抽象与操作系统的文件系统得以解耦。
现在,业务程序就可以用标准库io/fs定义的FS接口进行I/O操作,而无须直接依赖于os包,这为编写更加灵活和可移植的代码铺平了道路。
接下来介绍Go语言的FS接口定义。这些接口定义在io/fs包中,它们同样遵循最小接口的设计原则,旨在通过组合较小的接口来实现更强大的功能。
1.基础接口
fs.FS接口代表了文件系统,按照Go语言的理解,最基本的文件系统应至少包含一个Open方法。io.FS接口的定义如代码清单3-20所示。
代码清单3-20 io.FS接口的定义
在Open方法中,返回的是fs.File接口类型。fs.File接口代表一个文件,fs.File接口的定义如代码清单3-21所示。
代码清单3-21 fs.File接口的定义
通过fs.File接口定义可以看到,一个文件系统的文件至少需要提供Stat、Read、Close三种方法。对于File.Stat方法返回的fs.FileInfo也是接口类型,它代表文件的元数据信息。fs.FileInfo接口的定义如代码清单3-22所示。
代码清单3-22 fs.FileInfo接口的定义
以上定义的三个接口构成了一个极简的只读文件系统的框架。文件系统至少要有一个Open方法,返回一个可读的文件。我们可以在此基础之上扩展FS的接口,来丰富文件系统的功能。
2.扩展文件系统
Go语言的文件系统允许通过扩展来满足更丰富的应用场景。接下来看几个io/fs中基于fs.FS的扩展的文件系统。
(1)ReadDirFS
在io/fs包内定义一个名为ReadDirFS的接口,该文件系统基于FS增加了一个ReadDir方法,代表一个能够读取目录内容的文件系统。ReadDirFS接口的定义如代码清单3-23所示。
代码清单3-23 ReadDirFS接口的定义
(2)GlobFS
在io/fs包内定义一个名为GlobFS的接口,该文件系统基于FS增加了Glob方法,代表一个具备路径通配符查询的文件系统。GlobFS接口的定义如代码清单3-24所示。
代码清单3-24 GlobFS接口的定义
(3)StatFS
在io/fs定义一个名为StatFS的接口,该文件系统基于FS之上增加了Stat方法,代表一个路径查询的文件系统。StatFS接口的定义如代码清单3-25所示。
代码清单3-25 StatFS接口的定义
(4)ReadFileFS
在io/fs定义一个名为ReadFileFS的接口,该文件系统基于FS之上增加了ReadFile方法,代表一个可以通过路径读取文件内容的文件系统。ReadFileFS接口的定义如代码清单3-26所示。
代码清单3-26 ReadFileFS接口的定义
图3-4形象地展示了Go语言的扩展文件系统的拓扑关系。
图3-4 Go语言的FS拓扑关系
在本节内容中,我们将通过两个具体实例深入探讨Go语言是如何实现文件系统的:一种是代表操作系统的Dir文件系统,另一种是embed文件系统。这两种文件系统的类型都是对fs.FS接口的具体实现。
1.Dir文件系统
io/fs中定义了文件系统的fs.FS接口之后,Go语言紧接着就在os包里实现了这个接口,让os相关的文件操作兼容到fs.FS的接口上。首先,os包提供了一个DirFS函数,返回一个dirFS类型的结构体对象。DirFS函数的实现如代码清单3-27所示。
代码清单3-27 DirFS函数的实现
dirFS类型实现了fs.FS的Open方法,dirFS类型的定义和Open方法的实现如代码清单3-28所示。
代码清单3-28 dirFS类型的定义和Open方法的实现
在dirFS.Open方法中,调用了os.Open方法,获取到os.File的结构体的指针。而os.File类型是实现了fs.File接口的所有方法。这样就可以让os的文件操作兼容fs.FS的接口。
举一个实际读取并打印文件内容到控制台的例子。首先,我们需要创建一个名为hello.txt的文件,其内容为hello world。如果用传统的方式来读该文件,则如代码清单3-29所示。
代码清单3-29 使用os.Open函数读文件示例
如果针对上述代码中的PrintFile函数做单测,就需要实际创建出一个*os.File实例并传入。现在,我们可以改用接口进行重构,如代码清单3-30所示。
代码清单3-30 Dir文件系统读文件示例
通过类似的方法就可以比较平滑地重构代码了。把一些依赖于*os.File的参数替换掉。之后对PrintFile函数做单测就非常灵活了。
2.embed文件系统
Go语言为了更便捷地部署,提供了一个内嵌文件的功能。该功能允许开发者将静态文件直接打包进编译后的二进制程序中。这项特性意味着应用程序和它所需要的数据整合到单个二进制文件中,极大地简化了发布和部署过程。
Go标准库的embed包提供了一个内嵌文件系统(简称embed FS),通过它我们可以在运行时像操作普通文件一样读取这些嵌入二进制的静态文件。
首先,在embed包,我们定义一个名为FS的结构体,它具体实现了fs.FS接口的所有方法。embed的FS结构体定义如代码清单3-31所示。
代码清单3-31 embed的FS结构体定义
根据fs.FS的接口的定义,embed包的FS结构体仅需实现Open方法。代码清单3-32展示了embed的FS结构体的Open方法实现细节。
代码清单3-32 embed的FS结构体的Open方法
在embed的FS.Open方法中,首先通过lookup方法在files数组中根据名字查找是否存在内嵌文件,如果找到,就返回一个openFile的结构体实例。进一步地,openFile结构则要实现fs.File的接口,openFile的定义和实现如代码清单3-33所示。
代码清单3-33 openFile的定义和实现
openFile结构体实现了Read、Stat、Close方法,这就是一个完整的Go语言FS实现。
下面通过一个实际的示例来展示embed的FS使用技巧。首先,创建一个名为hello.txt的文件,其内容为“hello world”。然后,把这个文件在编译时内嵌到二进制中,如代码清单3-34所示。
代码清单3-34 embed文件系统的使用示例
编译之后hello.txt文件的内容就被嵌入二进制文件中。在程序运行时我们可以像读取普通文件一样读取hello.txt文件,而实际上,读取的是内嵌在二进制文件的数据,而非磁盘上的实体文件。