GO文件系统接口,草稿设计

2020-08-06 05:43:52

这是一个设计草案,而不是一个正式的GO提案,因为它描述了一个潜在的大型更改,标准库中的多个包以及潜在的第三方包中都需要集成更改。分发此设计草案的目的是收集反馈以形成预期的最终提案。

我们正在利用这一变化来试验新的方法来扩大关于重大变化的讨论。对于这一变化,我们将使用Go Reddit线程来管理问答,因为Reddit的线程支持可以轻松地将问题与答案匹配,并将不同的讨论线分开。

我们提出了一种新的GO标准库包IO/FS的可能设计,其定义了用于只读文件树的接口。我们还提供了将新包集成到标准库中的更改。

此软件包的部分动机是希望在go命令中添加对嵌入式文件的支持。请参阅嵌入式文件的设计草稿。

命名文件的分层树可作为各种资源的方便、有用的抽象,如Unix、Plan9和HTTP REST习惯用法所示。即使仅限于提取磁盘块,文件树也有多种形式:本地操作系统文件、存储在其他计算机上的文件、内存中的文件以及其他文件(如ZIP存档)中的文件。

Go得益于对单个文件中的数据进行良好的抽象,例如io.Reader、io.Writer和相关接口。这些都已经在围棋生态系统中得到了广泛的实施和使用。特定的读取器或写入器可以是操作系统文件、网络连接、内存中的缓冲区、ZIP存档中的文件、HTTP响应正文、存储在云服务器上的文件或许多其他内容。通用的、商定的接口支持创建有用的通用操作,如压缩、加密、散列、合并、拆分和复制,这些操作适用于所有这些不同的资源。

Go还可以从文件系统树的良好抽象中获益。公共的、达成一致的接口将有助于将可能作为文件系统表示的许多不同资源与可以在抽象之上实现的许多有用的通用操作相连接。

我们几年前就开始探索文件系统抽象的概念,在godoc中使用了内部抽象。该代码后来被提取为golang.org/x/tools/godoc/vfs,并启发了一些类似的包。该界面及其后继者似乎过于复杂,不可能是正确的通用抽象,但它们帮助我们更多地了解了设计可能是什么样子。在其间的几年中,我们也学到了更多关于如何使用接口来对更复杂的资源建模的知识。

在第5636和14106号问题上已经有过关于文件系统接口的讨论。

该设计的核心是定义文件系统抽象的新的包io/fs。虽然初始接口仅限于只读文件系统,但该设计可以扩展为支持以后的写入操作,即使是来自第三方软件包的写入操作也是如此。

此设计还考虑对archive/zip、html/template、net/http、os和text/template包进行微小调整,以更好地实现或使用文件系统抽象。

FS接口定义了实现的最低要求:只需一个Open方法。正如我们将看到的,FS实现还可以提供其他方法来优化操作或添加新功能,但只需要Open。

(因为包名是fs,所以我们需要为通用文件系统建立一个不同的典型变量名。原型代码使用fsys,此设计草案中的示例也是如此。只有在操作任意文件系统的代码中才需要这样的通用名称;大多数客户端代码将根据文件系统包含的内容使用有意义的名称,例如包含CSS文件的文件系统的样式。)。

所有FS实现都使用相同的名称语法:路径是无根的、以斜杠分隔的路径元素序列,类似于没有前导斜杠的unix路径,或者类似于没有前导http://host/.的URL。同样像在URL中一样,分隔符在所有系统上都是正斜杠,甚至在Windows上也是如此。可以使用路径包操作这些名称。FS路径名从不包含‘.’或“..”元素,但给定FS文件树的根目录名为“.”的特殊情况除外。路径可能区分大小写,也可能不区分大小写,具体取决于实现,因此客户端通常不应依赖于这一种行为或另一种行为。

使用无根名称-x/y/z.jpg而不是/x/y/z.jpg-是为了表明该名称只有在相对于特定的文件系统根进行解释时才有意义,该文件系统根没有在名称中指定。换句话说,缺少前导斜杠表明这些不是主机文件系统路径,也不是其他全局名称空间中的标识符。

File接口定义了实施的最低要求。对于File,这些要求是Stat、Read和Close,其含义与*os.File相同。File实现还可以提供其他方法来优化操作或添加新功能-例如,*os.File是有效的File实现-但只有这三种方法是必需的。

如果File表示一个目录,那么就像*os.File一样,Stat返回的FileInfo将从IsDir()(和Mode().IsDir())返回true。在这种情况下,File还必须实现ReadDirFile接口,该接口添加ReadDir方法。ReadDir方法与*os.File Readdr方法具有相同的语义,(稍后)此设计将大写为D的ReadDir添加到*os.File中。)。

//ReadDirFile是实现ReadDir方法进行目录读取的File,type ReadDirFile接口{File ReadDir(N Int)([]os.FileInfo,error)}。

这个ReadDirFile接口是一个旧的GO模式的示例,我们以前从未命名过它,但是我们建议调用一个扩展接口。扩展接口嵌入基本接口并添加一个或多个额外方法,作为指定可由基本接口的实例提供的可选功能的一种方式。

扩展接口的命名方式是在基接口名前面加上新方法:带有ReadDir的File is a ReadDirFile(带有ReadDir的文件是ReadDirFile)。请注意,可以将此约定视为现有名称(如io.ReadWriter和io.ReadWriteCloser)的泛化。也就是说,io.ReadWriter是也有Read方法的io.Writer,就像ReadDirFile是也有ReadDir方法的File一样。

Io/fs包没有定义ReadAtFile、ReadSeekFile等扩展名,以避免与io包重复。期望客户端直接使用IO接口来进行这样的操作。

扩展接口可以提供对基本接口中不可用的新功能的访问,或者扩展接口还可以使用基本接口,使用附加的方法调用来提供对已经可用的功能的更有效实现的访问。无论采用哪种方式,将扩展接口与帮助器函数配对都会很有帮助,该帮助器函数使用优化的实现(如果可用),否则会后退到基接口中可能出现的情况。

此扩展模式的早期示例-扩展接口与帮助器函数配对-是io.StringWriter接口和io.WriteString帮助器函数,它们从Go 1:

Package io//StringWriter是包装WriteString方法的接口。type StringWriter接口{WriteString(S String)(n int,err error)}//WriteString将字符串s的内容写入w,w接受一段字节。//如果w实现StringWriter,则直接调用其WriteString方法。//否则,w.Write将被精确调用once.func WriteString(w Writer,s String)(n int,err error){if。确定{return sw.WriteString(S)}返回w.Write([]字节)}

本例与上面的讨论不同,因为StringWriter不是一个扩展接口:它没有嵌入io.Writer。对于扩展方法替换原始方法的单方法接口,不重复原始方法是有意义的,就像这里一样。但一般来说,我们确实嵌入了原始接口,以便测试新接口的代码可以使用单个变量访问原始方法和新方法。(在本例中,StringWriter没有嵌入io.Writer意味着WriteString不能调用sw.Write。在这种情况下这没问题,但如果io.ReadSeeker不存在,请考虑一下:代码必须测试io.seeker,并为读取和查找操作使用单独的变量。)。

文件只有一个扩展接口,部分原因是为了避免与io中的现有接口重复。但FS有几个。

一个常见的操作是读取整个文件,就像ioutil.ReadFile对操作系统文件所做的那样。Io/fs包使用扩展模式提供此功能,它定义了一个受可选ReadFileFS接口支持的ReadFile帮助器函数:

ReadFile的一般实现可以调用fs.Open来获取File类型的文件,然后调用file.Read,最后调用file.Close。但是,如果FS实现可以在单个调用中更高效地提供文件内容,则它可以实现ReadFileFS接口:

顶层函数ReadFile首先检查其参数fs是否实现了ReadFileFS。如果是,则func ReadFile调用fs.ReadFile。否则,它将退回到打开、读取、关闭序列。

Func ReadFile(fsys FS,name string)([]byte,error){if fsys,ok:=fsy.(ReadFileFS);ok{return fsys.ReadFile(Name)}file,err:=fsys.Open(Name)if err!=nil{return nil,err}deer file.Close()return io.ReadAll(File)}。

Type StatFS interface{FS Stat(Name String)(os.FileInfo,error)}func Stat(fsys FS,name string)(os.FileInfo,error){if fsys,ok:=fsys.(Statfs);ok{return fsys.Stat(Name)}file,err:=fsys.Open(Name)if err!=nil{return nil,err}deer file.Close()return file.Stat()}。

Type ReadDirFS interface{FS ReadDir(Name String)([]os.FileInfo,Error)}函数ReadDir(fsys FS,Name String)([]os.FileInfo,Error)

实现遵循该模式,但回退情况稍微复杂一些:它必须通过创建要返回的适当错误来处理命名文件未实现ReadDirFile的情况。

Io/fs包提供了使用func ReadDir构建的顶级func Walk(类似于filepath.Walk),但没有类似的扩展接口。

Walk的语义是这样的,因此唯一重要的优化是能够访问快速的ReadDir函数。FS实现可以通过实现ReadDirFS来提供这一点。Walk的语义也相当微妙:拥有一个正确的实现比有错误的自定义实现更好,特别是在自定义实现不能提供任何重要优化的情况下。

这仍然可以看作是一种扩展模式,但是没有一对一的匹配:我们让Walk重用ReadDirFS,而不是使用WalkFS。

Type GlobFS interface{FS Glob(Pattern String)([]String,Error)}函数Glob(fsys FS,Pattern String)([]String,Error)。

这里的后备用例不是简单的单个调用,而是filepath.Glob的大部分副本:它必须决定读取哪些目录、读取它们并查找匹配的目录。

虽然Glob与Walk相似,因为它的实现是相当多的微妙代码,但Glob与Walk的不同之处在于,自定义实现可以提供显著的加速比。例如,假设模式为*/gopher.jpg。一般实现必须为ReadDir返回的列表中的每个目录调用ReadDir(";.";),然后调用Stat(dir+";/gopher.jpg";)。如果通过网络访问文件系统,并且*与许多目录匹配,则此序列需要多次往返。在这种情况下,FS可以实现一个Glob方法,该方法在单个往返行程中应答调用,只发送模式并只接收匹配项,从而避免所有不包含gopher.jpg的目录。

此设计仅限于上述操作,这些操作提供对文件系统的基本、方便、只读访问。但是,可以应用扩展模式来添加我们将来可能需要的任何新操作。即使是第三方软件包也可以使用它;并不是每个可能的文件系统操作都需要在io/fs中考虑。

例如,此设计中的文件系统不支持重命名文件。但是可以很容易地添加它,使用如下代码:

Type RenameFS interface{FS rename(oldpath,newpath string)error}func rename(fsys FS,oldpath,newpath string)error{if fsys,ok:=fsys.(RenameFS);ok{return fsys.Rename(oldpath,newpath)}return fmt.Errorf(";rename%s%s:operation not support";,oldpath,newpath)}。

请注意,此代码不需要位于io/fs包中。第三方包可以定义其自己的FS帮助器和扩展接口。

此设计中的FS也不提供打开要写入的文件的方式。同样,这可以使用扩展模式来完成,甚至可以从不同的包中完成。如果从不同的包中完成,代码可能如下所示:

Type OpenFileFS interface{fs.FS OpenFile(name string,flag int,perm os.FileMode)(fs.File,error)}func OpenFile(fsys FS,name string,flag int,perm os.FileMode)(fs.File,error){if fsys,ok:=fsys.(OpenFileFS);ok{return fsys.OpenFile(name,flag,perm)}if flag==os.O_RDONLY{return fs.。

请注意,即使此模式在多个其他包中实现,它们仍将全部互操作(假设方法签名匹配,这很有可能,因为包os已经定义了规范名称和签名)。互操作的结果是实现都同意共享文件系统类型和文件类型:fs.fs和fs.File。

扩展模式可以应用于任何缺少的操作:chmod、chtime、mkdir、mkdirAll、Sync等等。与将它们全部放入io/fs中不同,该设计从小规模开始,使用只读操作。

如上所述,io/fs包需要为os.FileInfo接口和os.FileMode类型导入os。这些类型并不真正属于操作系统,但是当他们被引入时,我们没有比他们更好的家了。现在,io/fs是一个更好的家,他们应该搬到那里去。

此设计将os.FileInfo和os.FileMode移到io/fs中,将os中的名称重新定义为io/fs中定义的别名。FileMode常量(如ModeDir)也会移动,将OS中的名称重新定义为复制IO/FS值的常量。不需要更新任何用户代码,但此移动将使只导入io/fs而不导入os来实现fs.FS成为可能。这类似于io不依赖于OS。(有关io不应该依赖于os的更多信息,请参阅“代码库重构(在GO的帮助下)”,特别是第3节。)。

出于同样的原因,类型os.PathError应该移到io/fs,并保留一个转发类型别名。

常规文件系统错误ErrInvalid、ErrPermission、ErrExist、ErrNotExist和ErrClosed也应移至io/fs。在本例中,这些是变量,而不是类型,因此不需要别名。包os中留下的定义如下:

为了匹配fs.ReadDirFile并固定大小写,设计添加了新的os.File方法ReadDir和ReadDirNames,等同于现有的Readdr和Readdrname。旧的大小写很久以前就应该更正;现在在os.File中更正它们比要求fs.File的所有实现使用错误的名称要好。(添加ReadDirNames并不是绝对必要的,但我们不妨同时修复它们。)。

最后,随着需要fs.FS接口的代码开始编写,自然会希望操作系统目录支持fs.FS。此设计增加了一个新函数os.DirFS:

软件包os//DirFS返回一个fs.FS实现,该实现//显示以dir.func DirFS(目录字符串)fs.FS为根的子树中的文件。

请注意,只有当FileInfo类型移到io/fs中时,才能编写此函数,这样os就可以导入io/fs,而不是反过来导入io/fs。

Html/template和text/template包各自提供了一对从操作系统文件系统读取的方法:

几乎所有的文件名都是仅与其自身匹配的GLOB模式,因此单个调用应该就足够了,而不必同时引入ParseFilesFS和ParseGlobFS。

类型文件系统接口{Open(名称字符串)(文件,错误)}类型文件接口{io.Closer io.Reader io.Seeker Readdr(Count Int)([]os.FileInfo,Error)Stat()(os.FileInfo,Error)}func FileServer(根文件系统)处理程序。

如果io/fs先于net/http,则此代码可以直接使用io/fs,从而消除了定义这些接口的需要。由于它们已经存在,因此必须保留它们以实现兼容性。

HandlerFS要求其文件系统打开的文件支持Seek。这是HTTP为支持范围请求而提出的额外要求。并非所有文件系统都需要实施Seek。

当前的zip.Reader没有Open方法,因此此设计添加了一个方法,带有实现fs.FS所需的签名。请注意,打开的文件是动态解压缩的字节流。它们可以读取,但不能查找。这意味着zip.Reader现在实现了fs.FS,因此可以用作传递给html/template的模板源。虽然也可以使用http.HandlerFS将相同的zip.Reader传递给net/http-也就是说,这样的程序将键入-check-HTTP服务器将无法为这些文件的范围请求提供服务,因为缺少查找方法。

另一方面,对于一小部分文件,定义在内存中缓存底层文件副本的文件系统中间件可能是有意义的,从而提供可搜索性并可能提高性能,以换取更高的内存使用率。这样的中间件-某种类型的CachingFS-可以在第三方包中提供,然后用于将zip.Reader连接到http.HandlerFS。实际上,启用这种中间件是此草案设计的一个关键目标。另一个例子可以是底层文件的透明解密。

该设计不包括对archive/tar的更改,因为该格式不能很容易地支持随机访问:第一次调用Open必须读取整个归档以查找其所有文件,并缓存该列表以供将来调用。即使底层的io.Reader支持Seek或ReadAt,这也是可能的。对于效率相当低的实现来说,这是大量的工作;将其添加到标准库将会设置性能陷阱。如果需要,该功能可以由第三方软件包提供。

具体设计决策的基本原理与上述决策一起给出。但是关于文件系统接口的讨论已经有很多年了,没有任何进展。为什么是现在?

首先,我们直接需要标准库中的功能,而需求仍然是发明之母。嵌入式文件草案设计旨在将对嵌入式文件的直接支持添加到GO命令中,这就提出了如何将它们与标准库的其余部分集成的问题。例如,嵌入式文件的常见用途是将其解析为模板或直接通过HTTP提供。如果没有这种设计,我们需要在这些包中定义接受嵌入文件的具体方法。定义文件系统接口让我们可以添加通用的新方法,这些方法不仅适用于嵌入式文件,还适用于ZIP文件和以FS实现形式呈现的任何其他类型的资源。

其次,我们对如何使用可选有更多的经验。

.