Redian新闻
>
Go 眼中的文件系统是什么? io.FS

Go 眼中的文件系统是什么? io.FS

公众号新闻


什么神奇问题 ?


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 文件系统的需求,可以有方法扩展了。

总结


  1. Go 在自己的层面封装出一个 io.FS 的抽象,意图和 OS 的 FS 解耦。这样可以给程序员带来更多的想象空间 ;
  2. embed FS 具备典型的 FS 的界面,但是它并不是直接位于 OS 的文件系统。所以它非常适合作为首个用 io.FS 的实践;
  3. 以后尽量用 io.FS 来管理的文件,这样可以做到和 OS 解耦,方便做单测;
  4. ioutil 可以少用,它的功能已经被移到更明确的 package 里实现了;

- EOF -

推荐阅读  点击标题可跳转

1、Golang 事件系统 Event Bus

2、Golang 中的异步任务队列

3、给 go 项目打最小 docker 镜像,足足降低 99%


↓推荐关注↓

「Go 开发大全」参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。关注后回复 Go 获取6万star的Go资源库


点赞和在看就是最大的支持❤️

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
钙离子通道(Calcium Channel)与心脏病下载2.25亿次、月流水破亿,分析师眼中Stumble Guys做对了什么?最新的卫生职称制度改革中的评价标准是什么?变局之下,年轻创业者眼中的未来思想周报丨华盛顿飓风眼中的古巴;争议中的卡塔尔世界杯美国法官下令任命特别主管审查在特朗普海湖庄园查获的文件买房风波(3)看看排名前十的高薪工作有哪些西方眼中的“普京大脑”,再成关注焦点linux 系统是怎么启动的?台湾人眼中的两岸你眼中的小兰花是仙女,我眼中的是…裸男!ArgoAI破产、特斯拉FSD落地,凛冬将至or腾飞在即?这么判断!Red Hat工程师开发新的 Linux文件系统A股跳水,下跌原因是什么?刚刚,国务院再发重磅文件!张文宏:新冠终将成为季节性流行病、为什么北方人感觉症状更严重早申即将截止,听听藤校招生官讲述“好”的文书是什么样的!乐活观察|《HR眼中的“水硕士”究竟什么样?》10/5 波士顿新闻汇总|东北大学爆炸事件系自导自演,职工面临联邦指控 一名高中学生被同学枪击 麻州的医疗保险费率将再涨7.6%使用 PSCP 将文件和文件夹从 Windows 传输到 Linux | Linux 中国宁可少活二十年拼命拿下大油田知识脱口秀:中国第一位尝到巧克力的皇帝是谁?八谷之冠指的是什么?人体最大的器官是什么?对话系统中的NLU 3.1 学术界中的方法(联合训练)VFS Global 杭州英国签证申请中心将搬迁新址!VFS Global 上海芬兰签证申请中心重新开业!川普任命的法官批准:可任命特别顾问审查川普带走的文件英国学生眼中的英国10大名校! 可能跟你想的不一样!西方的困局和出路关于 Python 的文件操作详解休斯顿金卡是什么?金卡申请条件是什么样?它与奥巴马保险和红兰卡区别在哪里?【直播预约】老板眼中的高效员工是什么样?阿里云EMR数据湖文件系统: 面向开源和云打造下一代 HDFS卫健委的文与饶老师的文:时间差八个月GNOME “文件”引入最受欢迎的功能:“新建文件”菜单 | Linux 中国美国文件如何公证认证?美国常见的文件认证有哪些?是什么在支撑特斯拉的技术演进?清华叉院赵行解读FSD算法新进展不同人眼中的记者,是什么样?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。