Go 1.16 io/fs 设计与实现及正确使用姿势

摘要

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包被废弃了,对应的函数在ioos中。

让我们来看看这个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这个包被废弃了,

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值