Go 语言标准库之 os 包

os 包提供了平台无关的操作系统功能接口,主要是文件相关的I/O,本文会重点对文件操作进行介绍。


文件 I/O

在 Go 中,文件描述符封装在os.File结构中。os.File代表一个打开的文件对象,可以使用该对象进行文件读写操作。

type File struct {
    *file // os specific
}

type file struct {
    pfd        poll.FD
    name       string
    dirinfo    *dirInfo // nil unless directory being read
    appendMode bool     // whether file is opened for appending
}

打开和关闭文件 OpenFile/Open/Close

☕️ OpenFile函数

OpenFile()是一个一般性的文件打开函数,大多数调用都应该使用Open()Create()代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如 0666 等)打开指定名称的文件。如果操作成功,返回的文件对象可用于 I/O。如果出错,错误底层类型是*PathError

func OpenFile(name string, flag int, perm FileMode) (file *File, err error)

此处需要特别介绍openFile()函数参数:

  • name:要打开的文件名,可以是一个绝对路径或相对路径,也可以是一个符号链接。

  • flag:指定文件的访问模式,可用值已在 os 包中定义为常量。模式有以下几种:

模式 含义
os.O_RDONLY 只读
os.O_WRONLY 只写(覆盖方式写)
os.O_RDWR 读写(覆盖方式写)
os.O_APPEND 往文件尾部追加方式写
os.O_CREATE 如果文件不存在则先创建
os.O_EXCL O_CREATE配合使用,文件必须不存在
os.O_SYNC 以同步I/O的方式打开文件
os.O_TRUNC 打开时清空文件

多种访问模式可以使用|操作符来连接,例如:O_RDWR|O_CREATE|O_TRUNC。其中,O_RDONLYO_WRONLYO_RDWR三种模式只能指定一个。

  • perm:指定了文件的模式和权限位,类型是os.FileMode

这些字位在所有的操作系统都有相同的含义,因此文件的信息可以在不同的操作系统之间安全的移植。不是所有的位都能用于所有的系统,唯一共有的是用于表示目录的ModeDir位。

type FileMode uint32

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 权限位(用于通过 & 获取类型位)
)

文件的权限打印出来一共十个字符。文件有三种基本权限:r(read,读权限)、w(write,写权限)、x(execute,执行权限),具体如下:

                                 - rwx rw- r--
  • 第 1 位:文件类型(d为目录,-为普通文件)

  • 第 2-4 位:所属用户权限,用 u(user)表示

  • 第 5-7 位:所属组权限,用 g(group)表示

  • 第 8-10 位:其他用户权限,用 o(other)表示

文件的权限还可以用八进制表示:r 表示为 4,w 表示为 2,x 表示为 1,- 表示为 0。例如:

  • 0777:权限- rwx rwx rwx的八进制表示,任何人都可读写,可执行
  • 0666:权限- rw- rw- rw-的八进制表示,任何人都可读写,但不可执行

⭐️ Open 函数

// 打开一个文件只能用于读取
func Open(name string) (file *File, err error)

该函数内部实际调用openFile()函数,源码如下:

func Open(name string) (*File, error) {
    // 以 os.O_RDONLY(只读)模式打开文件
    return OpenFile(name, O_RDONLY, 0)
}

✏️ Close 函数

// 关闭文件 f,使文件不能用于读写。它返回可能出现的错误
func (f *File) Close() error

close()用于关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当进程终止时,也会自动关闭其已打开的所有文件描述符。通常情况下,我们应该主动关闭文件描述符,如果不关闭,长期运行的服务可能会把文件描述符耗尽。关于返回值 error,以下两种情况会导致Close()返回错误:

  • 关闭一个未打开的文件
  • 两次关闭同一个文件

因此,通常我们不会去检查Close()返回的错误。

📚 示例代码

package main

import (
    "os"
)

func main() {
    // 以只读的方式打开文件
    file1, err := os.Open("./test1.txt")
    if err != nil {
        panic(err)
    }
    defer file1.Close()

    // 自定义方式打开文件
    file2, err := os.OpenFile("./test2.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
    if err != nil {
        panic(err)
    }
    defer file2.Close()
}

创建文件 Create

// 采用 0666 权限(任何人都可读写,不可执行)创建一个名为 name 的文件,如果文件已存在会截断它(为空文件)
// 如果成功,返回的文件对象可用于 I/O;对应的文件描述符具有 O_RDWR 模式。如果出错,错误底层类型是 *PathError
func Create(name string) (file *File, err error)

Create()函数本质上是调用OpenFile()函数,源码如下:

func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

✌ 示例代码

package main

import (
    "os"
)

func main() {
    // 以 O_RDWR|O_CREATE|O_TRUNC 模式,0666 权限打开文件
    newFile, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer newFile.Close()
}

读文件 Read/ReadAt

// 从 f 中读取最多 len(b) 字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误
// 文件终止标志是读取 0 个字节,且返回值 err 为 io.EOF
func (f *File) Read(b []byte) (n int, err error)

// 从指定的位置(相对于文件开始位置)读取 len(b) 字节数据并写入 b。它返回读取的字节数和可能遇到的任何错误
// 当 n < len(b) 时,本方法总是会返回错误;如果是因为到达文件结尾,返回值 err 会是 io.EOF
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

Read()ReadAt()的区别:前者从文件当前偏移量处读,且会改变文件当前的偏移量;而后者从 off 指定的位置开始读,且不会改变文件当前偏移量。

✍ 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 打开文件,只读
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 从文件当前偏移量读取 len(b) 字节,会改变文件当前的偏移量
    b := make([]byte, 5)
    n, err := file.Read(b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Number of bytes read: %d\n", n)
    fmt.Printf("Data read: %s\n", b)

    // 从文件指定偏移量读取 len(b) 字节,不会改变文件当前偏移量
    n, err = file.ReadAt(b, 0)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Number of bytes read: %d\n", n)
    fmt.Printf("Data read: %s\n", b)

    // 从当前文件偏移量继续读
    n, err = file.Read(b)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Number of bytes read: %d\n", n)
    fmt.Printf("Data read: %s\n", b)
}

// 文件 test.txt 内容:
// hello World

// 控制台输出:
// Number of bytes read: 5
// Data read: hello       
// Number of bytes read: 5
// Data read: hello       
// Number of bytes read: 5
// Data read:  Worl  

写文件 Write/WriteAt/WriteString/Sync

// 向文件中写入 len(b) 字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值 n != len(b),本方法会返回一个非 nil 的错误
func (f *File) Write(b []byte) (n int, err error)

// 在指定的位置(相对于文件开始位置)写入 len(b) 字节数据
// 它返回写入的字节数和可能遇到的任何错误。如果返回值 n != len(b),本方法会返回一个非 nil 的错误
func (f *File) WriteAt(b []byte, off int64) (n int, err error)

// 类似 Write,但接受一个字符串参数
func (f *File) WriteString(s string) (ret int, err error)

Write()WriteAt()的区别:前者从文件当前偏移量处写,且会改变文件当前的偏移量;而后者从 off 指定的位置开始写,且不会改变文件当前偏移量。

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

  • 打开文件时指定os.O_SYNC模式;
  1. 调用File.Sync()方法,该方法底层是 fsync 系统调用,这会将数据和元数据都刷到磁盘。
// 递交文件的当前内容进行稳定的存储。一般来说,这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存
func (f *File) Sync() (err error)

✍ 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 可写方式打开文件
    file, err := os.Create("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 在文件当前偏移量写入字节切片,会改变文件当前文件的偏移量
    n, err := file.Write([]byte("Hello World!\n"))
    if err != nil {
        panic(err)
    }
    fmt.Printf("Wrote %d bytes.\n", n)

    // 在文件当前偏移量写入字节切片,不会改变当前文件的偏移量
    n, err = file.WriteAt([]byte("gophers\n"), 0)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Wrote %d bytes.\n", n)

    // 在文件当前偏移量继续写入字符串
    n, err = file.WriteString("您好!")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Wrote %d bytes.\n", n)

    // 将数据刷新到磁盘
    file.Sync()
}

// 控制台输出:
// Wrote 13 bytes.
// Wrote 7 bytes.
// Wrote 9 bytes.

// 文件 text.txt 内容:
// gophersorld!
// 您好!

改变文件偏移量 Seek

于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个 Read 或 Write 操作的文件真实位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。

// 设置下一次读/写的文件偏移量。offset 为相对偏移量,而 whence 决定相对位置:0 为相对文件开头,1 为相对当前位置,2 为相对文件结尾
// 它返回新的偏移量(相对开头)和可能的错误。
func (f *File) Seek(offset int64, whence int) (ret int64, err error)

// whence 的值,在 io 包中定义了相应的常量,推荐使用这些常量
const (
  SeekStart   = 0 // seek relative to the origin of the file
  SeekCurrent = 1 // seek relative to the current offset
  SeekEnd     = 2 // seek relative to the end
)

💡 示例代码

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // 相对偏移量,可以是正数也可以是负数
    var offset int64 = 5

    // 相对位置
    // io.SeekStart = 0   // 文件开头
    // io.SeekCurrent = 1 // 当前位置
    // io.SeekEnd = 2     // 文件结尾
    var whence = io.SeekStart
    newPosition, err := file.Seek(offset, whence)
    if err != nil {
        panic(err)
    }
    fmt.Println("Just moved to 5:", newPosition)

    // 从当前位置回退两个字节
    newPosition, err = file.Seek(-2, io.SeekCurrent)
    if err != nil {
        panic(err)
    }
    fmt.Println("Just moved back two:", newPosition)

    // 使用下面的技巧得到当前的位置
    currentPosition, err := file.Seek(0, io.SeekCurrent)
    fmt.Println("Current position:", currentPosition)

    // 转到文件开始处
    newPosition, err = file.Seek(0, io.SeekStart)
    if err != nil {
        panic(err)
    }
    fmt.Println("Position after seeking 0,0:", newPosition)
}
// 文件 test.txt 内容:
// hello world

// 控制台输出:
// Just moved to 5: 5
// Just moved back two: 3
// Current position: 3
// Position after seeking 0,0: 0

裁剪文件 Truncate

Truncate()用于将文件的长度设置为 size 参数指定的值。如果文件当前长度大于参数 size,将丢弃超出部分;若小于参数 size,将在文件尾部添加一系列 null 字节或是一个文件空洞进行填充。

// 修改 name 指定的文件的大小
// 如果该文件为一个符号链接,将修改链接指向的文件的大小。如果出错,会返回 *PathError 底层类型的错误
func Truncate(name string, size int64) error

// 改变文件的大小,它不会改变 I/O 的当前位置
// 如果截断文件,多出的部分就会被丢弃。如果出错,错误底层类型是 *PathError
func (f *File) Truncate(size int64) error

它们之间的区别在于如何指定操作文件:

  • Truncate()以路径名称字符串来指定文件,并要求可访问该文件(即对组成路径名的各目录拥有可执行 (x) 权限),且对文件拥有写权限。若文件名为符号链接,那么调用将对其进行解引用。
  • 很明显,调用File.Truncate()前,需要先以可写方式打开操作文件,该方法不会修改文件偏移量。

☕️ 示例代码

package main

import (
   "os"
)

func main() {
   // 裁剪一个文件到 100 个字节
   // 如果文件本来就少于 100 个字节,则文件中原始内容得以保留,剩余的字节以 null 字节填充
   // 如果文件本来超过 100 个字节,则超过的字节会被抛弃
   // 传入 0 则会清空文件

   // 方式一:
   err := os.Truncate("./test1.txt", 20)
   if err != nil {
       panic(err)
   }

   // 方式二:
   // 打开文件模式必须包括 os.O_WRONLY 或者 os.O_RDWR,即可写
   file, err := os.OpenFile("./test2.txt", os.O_WRONLY, 0666)
   if err != nil {
       panic(err)
   }
   defer file.Close()
   err = file.Truncate(20)
   if err != nil {
       panic(err)
   }
}

文件属性 os.FileInfo

文件属性,也即文件元数据。在 Go 中,文件属性具体信息通过os.FileInfo接口获取,文件的信息包括文件名、文件大小、修改权限、修改时间等。

type FileInfo interface {
    Name() string       // 文件的名字(不含扩展名)
    Size() int64        // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
    Mode() FileMode     // 文件的权限
    ModTime() time.Time // 文件的修改时间
    IsDir() bool        // 等价于 Mode().IsDir()
    Sys() interface{}   // 底层数据来源(可以返回 nil)
}

os.FileMode类型表示文件的模式和权限位,在介绍OpenFile()函数的时候提及过,其方法如下:

type FileMode uint32

// 报告 m 是否是一个目录
func (m FileMode) IsDir() bool

// 报告 m 是否是一个普通文件
func (m FileMode) IsRegular() bool

// 返回 m 的 Unix 权限位
func (m FileMode) Perm() FileMode

// 打印出文件权限
func (m FileMode) String() string

// 返回文件类型
func (m FileMode) Type() FileMode {

获取文件信息 Stat/Lstat

// 返回一个描述 name 指定的文件对象的 FileInfo
// 如果指定的文件对象是一个符号链接,返回的 FileInfo 描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为 *PathError 类型
func Stat(name string) (fi FileInfo, err error)

// 返回一个描述 name 指定的文件对象的 FileInfo
// 如果指定的文件对象是一个符号链接,返回的 FileInfo 描述该符号链接的信息,本函数不会试图跳转该链接。如果出错,返回的错误值为 *PathError 类型
func Lstat(name string) (fi FileInfo, err error)

// 返回描述文件 f 的 FileInfo 类型值。如果出错,错误底层类型是 *PathError
func (f *File) Stat() (fi FileInfo, err error)

函数Stat()Lstat()File.Stat()可以得到os.FileInfo接口的实例,分别对应三个系统调用:stat、lstat 和 fstat。它们区别在于:

  • Stat()会返回所命名文件的相关信息;

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

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

Stat()Lstat()无需对其所操作的文件本身拥有任何权限,但针对指定 name 的父目录要有执行(搜索)权限。而只要 File 对象 ok,File.Stat()总是成功。

⭐️ 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 方式一:
    fileInfo1, err := os.Stat("./test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println("文件名:", fileInfo1.Name())         // test.txt
    fmt.Println("文件大小:", fileInfo1.Size())        // 11
    fmt.Println("文件权限:", fileInfo1.Mode())        // -rw-rw-rw-
    fmt.Println("文件最后修改时间:", fileInfo1.ModTime()) // 2021-12-21 19:11:15.686021 +0800 CST
    fmt.Println("是否为目录:", fileInfo1.IsDir())      // false

    // 方式二:
    fileInfo2, err := os.Lstat("./test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println("文件名:", fileInfo2.Name())         // test.txt
    fmt.Println("文件大小:", fileInfo2.Size())        // 11
    fmt.Println("文件权限:", fileInfo2.Mode())        // -rw-rw-rw-
    fmt.Println("文件最后修改时间:", fileInfo2.ModTime()) // 2021-12-21 19:11:15.686021 +0800 CST
    fmt.Println("是否为目录:", fileInfo2.IsDir())      // false

    // 方式三:
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    fileInfo3, err := file.Stat()
    if err != nil {
        panic(err)
    }
    fmt.Println("文件名:", fileInfo3.Name())         // test.txt
    fmt.Println("文件大小:", fileInfo3.Size())        // 11
    fmt.Println("文件权限:", fileInfo3.Mode())        // -rw-rw-rw-
    fmt.Println("文件最后修改时间:", fileInfo3.ModTime()) // 2021-12-21 19:11:15.686021 +0800 CST
    fmt.Println("是否为目录:", fileInfo3.IsDir())      // false
}

改变时间戳 Chtimes

// 修改 name 指定的文件对象的访问时间和修改时间,类似 Unix 的 utime() 或 utimes() 函数
// 底层的文件系统可能会截断/舍入时间单位到更低的精确度。如果出错,会返回 *PathError 底层类型的错误
func Chtimes(name string, atime time.Time, mtime time.Time) error

📚 示例代码

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    fileInfo, err := os.Stat("./test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println("最后修改时间:", fileInfo.ModTime())

    // 改变文件时间戳为两天前
    twoDaysFromNow := time.Now().Add(48 * time.Hour)
    lastAccessTime := twoDaysFromNow
    lastModifyTime := twoDaysFromNow
    err = os.Chtimes("./test.txt", lastAccessTime, lastModifyTime)
    if err != nil {
        panic(err)
    }
    fileInfo, err = os.Stat("./test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println("最后修改时间:", fileInfo.ModTime())
}

改变拥有者 Chown

// 修改 name 指定的文件对象的用户 id 和组 id
// 如果 name 指定的文件是一个符号链接,它会修改该链接的目的地文件的用户 id 和组 id。如果出错,会返回 *PathError 底层类型的错误
func Chown(name string, uid, gid int) error

// 修改 name 指定的文件对象的用户 id 和组 id
// 如果 name 指定的文件是一个符号链接,它会修改该符号链接自身的用户 id 和组 id。如果出错,会返回 *PathError 底层类型的错误
func Lchown(name string, uid, gid int) error

// 修改文件的用户 ID 和组 ID。如果出错,错误底层类型是 *PathError
func (f *File) Chown(uid, gid int) error

它们的区别和上文提到的 Stat 相关函数类似。

✏️ 示例代码

package main

import (
    "os"
)

func main() {
    // 方式一
    err := os.Chown("./test.txt", os.Getuid(), os.Getgid())
    if err != nil {
        panic(err)
    }

    // 方式二
    err = os.Lchown("./test.txt", os.Getuid(), os.Getgid())
    if err != nil {
        panic(err)
    }

    // 方式三
    file, err := os.Open("./test2.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    err = file.Chown(os.Getuid(), os.Getgid())
    if err != nil {
        panic(err)
    }
}

改变文件权限 Chmod

// 修改 name 指定的文件对象的 mode
// 如果 name 指定的文件是一个符号链接,它会修改该链接的目的地文件的 mode。如果出错,会返回 *PathError 底层类型的错误。
func Chmod(name string, mode FileMode) error

// 修改文件的模式。如果出错,错误底层类型是 *PathError
func (f *File) Chmod(mode FileMode) error

✌ 示例代码

package main

import (
    "os"
)

func main() {
    // 方式一:
    // 改变文件权限(windows不支持该函数)
    err := os.Chmod("./test.txt", 0777)
    if err != nil {
        panic(err)
    }

    // 方式二:
    file, err := os.Open("./test.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    // 改变文件权限(windows不支持该函数)
    err = file.Chmod(0777)
    if err != nil {
        panic(err)
    }
}

检查读写权限 IsPermission

// 返回一个布尔值说明该错误是否表示因权限不足要求被拒绝。ErrPermission 和一些系统调用错误会使它返回真
func IsPermission(err error) bool

✍ 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 这个例子测试写权限,如果没有写权限则返回 error
    // 注意文件不存在也会返回 error,需要检查 error 的信息来获取到底是哪个错误导致
    file1, err := os.OpenFile("./test.txt", os.O_RDONLY, 0666)
    if err != nil {
        panic(err)
    }
    defer file1.Close()
    _, err = file1.WriteString("hello world")
    if os.IsPermission(err) {
        fmt.Println(err) // write ./test.txt: Access is denied.
    }

    // 测试读权限
    file2, err := os.OpenFile("./test.txt", os.O_WRONLY, 0666)
    if err != nil {
        panic(err)
    }
    defer file2.Close()
    _, err = file2.Read(make([]byte, 10))
    if os.IsPermission(err) {
        fmt.Println(err) // read ./test.txt: Access is denied.
    }
}

其它文件操作

创建目录 Mkdir/MkdirAll

// 使用指定的权限和名称创建一个目录。如果出错,会返回 *PathError 底层类型的错误
func Mkdir(name string, perm FileMode) error

// 使用指定的权限和名称创建一个目录,包括任何必要的上级目录,并返回 nil,否则返回错误。
// 权限位 perm 会应用在每一个被本函数创建的目录上。如果 path 指定了一个已经存在的目录,MkdirAll 不做任何操作并返回nil
func MkdirAll(path string, perm FileMode) error

💡 示例代码

package main

import (
    "os"
)

func main() {
    // 只能创建单级目录
    err := os.Mkdir("./test", os.ModePerm)
    if err != nil {
        panic(err)
    }

    // 能创建多级目录
    err = os.MkdirAll("./aa/bb", os.ModePerm)
    if err != nil {
        panic(err)
    }
}

删除文件或目录 Remove/RemoveAll

// 删除 name 指定的文件或目录(必须为空目录)。如果出错,会返回 *PathError 底层类型的错误
func Remove(name string) error

// 删除 path 指定的文件或目录及它包含的任何下级对象。它会尝试删除所有东西,除非遇到错误并返回
// 如果 path 指定的对象不存在,RemoveAll 会返回 nil 而不返回错误
func RemoveAll(path string) error

☕️ 示例代码

package main

import (
    "os"
)

func main() {
    // 删除文件或目录(必须为空目录)
    err := os.Remove("./test.txt")
    if err != nil {
        panic(err)
    }

    // 删除文件或者目录
    err = os.RemoveAll("./aa")
    if err != nil {
        panic(err)
    }
}

重命名和移动 Rename

// Rename 修改一个文件的名字,移动一个文件。可能会有一些操作系统特定的限制
func Rename(oldpath, newpath string) error

⭐️ 示例代码

package main

import (
    "os"
)

func main() {
    originalPath := "./test1.txt"
    newPath := "./test2.txt"
    err := os.Rename(originalPath, newPath)
    if err != nil {
        panic(err)
    }
}

检查文件是否存在 IsExist/IsNotExist

// 返回一个布尔值说明该错误是否表示一个文件或目录已经存在。ErrExist 和一些系统调用错误会使它返回真
func IsExist(err error) bool

// 返回一个布尔值说明该错误是否表示一个文件或目录不存在。ErrNotExist 和一些系统调用错误会使它返回真
func IsNotExist(err error) bool

✏️ 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 文件不存在则返回 error
    _, err := os.Open("./test1.txt")
    if os.IsNotExist(err) {
        fmt.Println("File does not exist.")
    }
}

硬链接:文件是通过索引节点(Inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,每添加一个一个硬链接,文件的链接数就加 1,只有所有的硬链接被删除后文件才会被删除。只有在同一文件系统中的文件之间才能创建硬链接,不能对目录进行创建,但是这个硬链接又可以建立多个,也就是可以有多个文件指向同一个索引节点,或者说一个文件可以拥有多个路径名,因此一个文件可以对应多个文件名。

软链接:和硬链接有点不一样,它不直接指向索引节点,而是通过名字引用其它文件,类似 Windows 的快捷方式。软链接可以指向不同的文件系统中的不同文件,但是并不是所有的操作系统都支持软链接。

// 创建一个名为 newname 指向 oldname 的硬链接。如果出错,会返回 *LinkError 底层类型的错误
func Link(oldname, newname string) error

// 创建一个名为 newname 指向 oldname 的软链接。如果出错,会返回 *LinkError 底层类型的错误
func Symlink(oldname, newname string) error

📚 示例代码

package main

import (
    "fmt"
    "os"
)

func main() {
    // 创建一个硬链接
    // 创建后同一个文件内容会有两个文件名,改变一个文件的内容会影响另一个
    // 删除和重命名不会影响另一个
    err := os.Link("./test1.txt", "./test1_also.txt")
    if err != nil {
        panic(err)
    }

    // 创建软链接(Windows不支持软链接)
    err = os.Symlink("./test1.txt", "./test1_sym.txt")
    if err != nil {
        panic(err)
    }

    // Lstat 返回一个文件的信息,但是当文件是一个软链接时,它返回软链接的信息,而不是引用的文件的信息
    fileInfo, err := os.Lstat("./test1_sym.txt")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Link info: %+v", fileInfo)

    // 改变软链接的拥有者不会影响原始文件
    err = os.Lchown("./test1_sym.txt", os.Getuid(), os.Getgid())
    if err != nil {
        panic(err)
    }
}

参考

  1. os — 平台无关的操作系统功能实现
  2. Go语言的30个常用文件操作,总有一个你会用到
posted @ 2021-12-22 16:21  呵呵233  阅读(2207)  评论(0编辑  收藏  举报