go标准包系列-文件系统相关包

go文件系统相关包

1 os包

os 包提供了平台无关的操作系统功能接口。尽管错误处理是 go 风格的,但设计是 Unix 风格的;所以,失败的调用会返回 error 而非错误码。通常 error 里会包含更多信息。例如,如果使用一个文件名的调用(如 Open、Stat)失败了,打印错误时会包含该文件名,错误类型将为 *PathError,其内部可以解包获得更多信息。

1.1 文件 I/O
打开一个文件:OpenFile
func OpenFile(name string, flag int, perm FileMode) (*File, error)

错误底层类型是 *PathError

name //是绝对路径或相对路径,也可以是一个符号链接
flag //指定文件的访问模式
perm //os.FileMode

flag值枚举

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和 O_CREATE 配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步 I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)

其中,O_RDONLYO_WRONLYO_RDWR 应该只指定一个,剩下的通过 | 操作符来指定。

perm值枚举

const (
    // 单字符是被 String 方法用于格式化的属性缩写。
    ModeDir        FileMode = 1 << (32 - 1 - iota) // d: 目录
    ModeAppend                                     // a: 只能写入,且只能写入到末尾
    ModeExclusive                                  // l: 用于执行
    ModeTemporary                                  // T: 临时文件(非备份文件)
    ModeSymlink                                    // L: 符号链接(不是快捷方式文件)
    ModeDevice                                     // D: 设备
    ModeNamedPipe                                  // p: 命名管道(FIFO)
    ModeSocket                                     // S: Unix 域 socket
    ModeSetuid                                     // u: 表示文件具有其创建者用户 id 权限
    ModeSetgid                                     // g: 表示文件具有其创建者组 id 的权限
    ModeCharDevice                                 // c: 字符设备,需已设置 ModeDevice
    ModeSticky                                     // t: 只有 root/ 创建者能删除 / 移动文件

    // 覆盖所有类型位(用于通过 & 获取类型位),对普通文件,所有这些位都不应被设置
    ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
    ModePerm FileMode = 0777 // 覆盖所有 Unix 权限位(用于通过 & 获取类型位)
)

其他方式打开文件对OpenFile的调用

func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
读取文件内容:Read
func (f *File) Read(b []byte) (n int, err error)
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
  • 实现了 io.Reader 接口。
  • Read 方法从 f 中读取最多 len(b) 字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取 0 个字节且返回值 err 为 io.EOF。Read对应的系统调用是read
  • ReadAt 从指定的位置(相对于文件开始位置)读取长度为 len(b) 个字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误。当 n<len(b) 时,本方法总是会返回错误;如果是因为到达文件结尾,返回值 err 会是 io.EOF。它对应的系统调用是 pread
  • ReadReadAt 的区别:前者从文件当前偏移量处读,且会改变文件当前的偏移量;而后者从 off 指定的位置开始读,且不会改变文件当前偏移量。
数据写入文件:Write
func (f *File) Write(b []byte) (n int, err error)
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
  • 实现了io.Writer 接口。
  • Write 向文件中写入 len(b) 字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值 n!=len(b),本方法会返回一个非 nil 的错误。Write对应的系统调用是write
  • WriteWriteAt 的区别同 ReadReadAt 的区别一样。为了方便,还提供了 WriteString 方法,它实际是对 Write 的封装。

强制写入磁盘

Write 调用成功并不能保证数据已经写入磁盘,因为内核会缓存磁盘的 I/O 操作。如果希望立刻将数据写入磁盘(一般场景不建议这么做,因为会影响性能),有两种办法:

1. 打开文件时指定 `os.O_SYNC`;
2. 调用 `File.Sync()` 方法。
关闭文件:Close
func (f *File) Close() error

os.File.Close() 是对 close() 的封装。

常见写法

file, err := os.Open("/tmp/studygolang.txt")
if err != nil {
    // 错误处理,一般会阻止程序往下执行
    return
}
defer file.Close()

以下两种情况会导致 Close 返回错误:

1. 关闭一个未打开的文件;
2. 两次关闭同一个文件;

通常,我们不会去检查 Close 的错误。

改变文件偏移量:Seek
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
//offset 为相对偏移量
//whence 决定相对位置 SEEK_SET、SEEK_CUR 和 SEEK_END
//代码举例
file.Seek(0, os.SEEK_SET)    // 文件开始处
file.Seek(0, SEEK_END)        // 文件结尾处的下一个字节
file.Seek(-1, SEEK_END)        // 文件最后一个字节
file.Seek(-10, SEEK_CUR)     // 当前位置前 10 个字节
file.Seek(1000, SEEK_END)    // 文件结尾处的下 1001 个字节
1.2.截断文件

trucateftruncate 系统调用将文件大小设置为 size 参数指定的值;Go 语言中相应的包装函数是 os.Truncateos.File.Truncate

func Truncate(name string, size int64) error
func (f *File) Truncate(size int64) error
1.3 文件属性

文件属性,也即文件元数据。在 Go 中,文件属性具体信息通过 os.FileInfo 接口获取。函数 StatLstatFile.Stat 可以得到该接口的实例。这三个函数对应三个系统调用:statlstatfstat

这三个函数的区别:

  • stat 会返回所命名文件的相关信息。

  • lstatstat 类似,区别在于如果文件是符号链接,那么所返回的信息针对的是符号链接自身(而非符号链接所指向的文件)。

  • fstat 则会返回由某个打开文件描述符(Go 中则是当前打开文件 File)所指代文件的相关信息。

FileInfo 接口如下:

type FileInfo interface {
    Name() string       // 文件的名字(不含扩展名)
    Size() int64        // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
    Mode() FileMode     // 文件的模式位
    ModTime() time.Time // 文件的修改时间
    IsDir() bool        // 等价于 Mode().IsDir()
    Sys() interface{}   // 底层数据来源(可以返回 nil)
}
改变文件时间戳
func Chtimes(name string, atime time.Time, mtime time.Time) error
文件属主
func Chown(name string, uid, gid int) error
func Lchown(name string, uid, gid int) error
func (f *File) Chown(uid, gid int) error
文件权限
func main() {
    file, err := os.Create("studygolang.txt")
    if err != nil {
        log.Fatal("error:", err)
    }
    defer file.Close()

    fileMode := getFileMode(file)
    log.Println("file mode:", fileMode)
    file.Chmod(fileMode | os.ModeSticky)

    log.Println("change after, file mode:", getFileMode(file))
}

func getFileMode(file *os.File) os.FileMode {
    fileInfo, err := file.Stat()
    if err != nil {
        log.Fatal("file stat error:", err)
    }

    return fileInfo.Mode()
}
1.4 目录与链接
创建和移除(硬)链接
func Link(oldname, newname string) error

func Remove(name string) error
更改文件名

Go 中的 os.Rename 是对应的封装函数。

func Rename(oldpath, newpath string) error
创建和移除目录
func Mkdir(name string, perm FileMode) error

func RemoveAll(path string) error
读目录
func (f *File) Readdirnames(n int) (names []string, err error)
func (f *File) Readdir(n int) (fi []FileInfo, err error)
  • Readdirnames 读取目录 f 的内容,返回一个最多有 n 个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用未读取的内容的信息。

  • Readdir内部会调用Readdirnames,将得到的 names构造路径,通过Lstat构造出[]FileInfo

2 path/filepath

2.1 解析路径名字符串
func Dir(path string) string //获取文件最后一级目录
func Base(path string) string //获取文件名
2.2. 相对路径和绝对路径
func IsAbs(path string) bool  //是否是一个绝对路径
func Abs(path string) (string, error) //返回 path 代表的绝对路径
func Rel(basepath, targpath string) (string, error) //返回一个相对路径
2.3 路径的切分和拼接
func Split(path string) (dir, file string) //Split 函数根据最后一个路径分隔符将路径 path 分隔为目录和文件名两部分(dir 和 file
2.4 规整化路径
func Clean(path string) string
2.5 遍历目录
func Walk(root string, walkFn WalkFunc) error

Walk 函数会遍历 root 指定的目录下的文件树,对每一个该文件树中的目录和文件都会调用 walkFn,包括 root 自身。所有访问文件 / 目录时遇到的错误都会传递给 walkFn 过滤。文件是按字典顺序遍历的,这让输出更漂亮,但也导致处理非常大的目录时效率会降低。Walk 函数不会遍历文件树中的符号链接(快捷方式)文件包含的路径。

walkFn 的类型 WalkFunc 的定义如下:

type WalkFunc func(path string, info os.FileInfo, err error) error

3 io/fs — 抽象文件系统

3.1 三个核心接口
fs.FS
type FS interface {
    Open(name string) (File, error)
}
//错误类型
type PathError struct {
    Op   string
    Path string
    Err  error
}

type PathError = fs.PathError //Go 1.16 后,os.PathError 只是 fs.PathError 的别名。
func ValidPath(name string) bool //对于指定的文件名,需要满足 ValidPath(name) 函数,如果不满足,则返回 *PathError 的 Err 为 fs.ErrInvalid 或 fs.ErrNotExist 的错误。
fs.File
type File interface {
    Stat() (FileInfo, error)
    Read([]byte) (int, error)
    Close() error
}
fs.FileInfo

该接口描述一个文件的元数据信息,它由 Stat 返回。为了方便,在 io/fs 包有一个 Stat 函数:

func Stat(fsys FS, name string) (FileInfo, error)

type FileInfo interface {
    Name() string       // 文件的名字(不含扩展名)
    Size() int64        // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
    Mode() FileMode     // 文件的模式位
    ModTime() time.Time // 文件的修改时间
    IsDir() bool        // 等价于 Mode().IsDir()
    Sys() interface{}   // 底层数据来源(可以返回 nil)
}
3.2 实现一个文件系统
实现 fs.File 和 fs.FileInfo
type file struct {
    name    string
    content *bytes.Buffer
    modTime time.Time
    closed  bool
}

func (f *file) Read(p []byte) (int, error) {
    if f.closed {
        return 0, errors.New("file closed")
    }

    return f.content.Read(p)
}

func (f *file) Stat() (fs.FileInfo, error) {
    if f.closed {
        return nil, errors.New("file closed")
    }

    return f, nil
}

// Close 关闭文件,可以调用多次。
func (f *file) Close() error {
    f.closed = true
    return nil
}

// 实现 fs.FileInfo

func (f *file) Name() string {
    return f.name
}

func (f *file) Size() int64 {
    return int64(f.content.Len())
}

func (f *file) Mode() fs.FileMode {
    // 固定为 0444
    return 0444
}

func (f *file) ModTime() time.Time {
    return f.modTime
}

// IsDir 目前未实现目录功能
func (f *file) IsDir() bool {
    return false
}

func (f *file) Sys() interface{} {
    return nil
}
实现 fs.FS
type FS struct {
    files map[string]*file
}

func NewFS() *FS {
    return &FS{
        files: make(map[string]*file),
    }
}

func (fsys *FS) Open(name string) (fs.File, error) {
    if !fs.ValidPath(name) {
        return nil, &fs.PathError{
            Op:   "open",
            Path: name,
            Err:  fs.ErrInvalid,
        }
    }

    if f, ok := fsys.files[name]; !ok {
        return nil, &fs.PathError{
            Op:   "open",
            Path: name,
            Err:  fs.ErrNotExist,
        }
    } else {
        return f, nil
    }
}
func (fsys *FS) WriteFile(name, content string) error {
    if !fs.ValidPath(name) {
        return &fs.PathError{
            Op:   "write",
            Path: name,
            Err:  fs.ErrInvalid,
        }
    }

    f := &file{
        name:    name,
        content: bytes.NewBufferString(content),
        modTime: time.Now(),
    }

    fsys.files[name] = f

    return nil
}
验证
func TestMemFS(t *testing.T) {
    name := "x/y/name.txt"
    content := "This is polarisxu, welcome."
    memFS := memfs.NewFS()
    err := memFS.WriteFile(name, content)
    if err != nil {
        t.Fatal(err)
    }

    f, err := memFS.Open(name)
    if err != nil {
        t.Fatal(err)
    }
    defer f.Close()

    fi, err := f.Stat()
    if err != nil {
        t.Fatal(err)
    }

    t.Log(fi.Name(), fi.Size(), fi.ModTime())

    var result = make([]byte, int(fi.Size()))
    n, err := f.Read(result)
    if err != nil {
        t.Fatal(err)
    }

    if string(result[:n]) != content {
        t.Errorf("expect: %s, actual: %s", content, result[:n])
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值