oldme 博客

躬自厚而薄责于人,则远怨矣

Go os 包介绍与使用

oldme create: 2023-06-06

Go 的 os 包提供了与操作系统相关的功能。提供了底层操作系统交互,执行文件操作,处理环境变量,管理进程、命令执行等功能。

前言

os 包提供了与操作操作系统交互的相关接口,其功能设计类似于 Unix。其中最重要的功能就是文件操作和目录操作,本文会重点介绍它们。

文件 I/O:打开与关闭文件

OpenFile

OpenFile 函数用于打开一个文件并返回一个文件对象:File,文件的读取、写入、创建等都会依赖于 File 对象,它是文件 I/O 操作的基石。OpenFile 函数定义如下:

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

name 是需要打开的文件文件名,可以是绝对路径和相对路径,也可以是一个符号链接(Liunx 的软链接,Windows 的快捷方式是不行的)。

flag 指定文件的访问模式,可用的值已在 os 包中定义为常量:

模式 含义
O_RDONLY 只读打开文件
O_WRONLY 只写打开文件
O_RDWR 读写打开文件
O_APPEND 文件尾部追加写
O_CREATE 如果文件不存在,则创建
O_EXCL 和 O_CREATE 一起使用,文件必须不存在
O_SYNC 打开文件以同步模式进行I/O:以同步模式打开文件,在每次写入数据时都会刷新磁盘,这样可以保证数据不丢失,但性能会降低。
O_TRUNC 打开时清空文件

多个 flag 可以使用 or 连接使用,如:O_RDWR | O_TRUNC | O_SYNC。Go 官方手册中说 flag 参数必须指定 O_RDONLY、O_WRONLY、O_RDWR 中的一个,但在 Go 1.20.4 中,即使它们三个不指定,某些情况下程序也是正常运行的,这点还需要更深的考证,代码如下:

// 如果demo.txt不存在,则创建它
func TestOpenFile(t *testing.T) {
	_, err := os.OpenFile("./demo.txt", os.O_CREATE, 0777)
	if err != nil {
		t.Fatal(err)
	}
}

perm 代表着当前的权限位,在所有操作系统中它们代表的含义都是一样的,这个参数可以传 os 包中已经定义好的常量:

权限    含义
ModeDir 目录
ModeAppend 只能写入且只能写在末尾
ModeExclusive 用于执行
ModeTemporary 临时文件
ModeSymlink 符号链接
ModeDevice 设备文件
ModeNamedPipe 命名管道
ModeSocket Unix 域 socket
ModeSetuid 文件具有其创建者用户 id 权限
ModeSetgid 文件具有其创建者组 id 的权限
ModeCharDevice Unix 字符设备,需要设置 ModeDevice
ModeSticky sticky 位
ModeIrregular 非常规文件
ModeType 包含ModeDir、ModeSymlink、ModeNamedPipe、ModeSocket、ModeDevice、ModeCharDevice、ModeIrregular
ModePerm 0777,八进制的777,即所有权限位

perm 也可以传自己定义的权限位,其方法和 Unix/Linux 权限一致,即我们熟知的 777、755、644等,不过在前面需要加个0,代表八进制。

os.OpenFile 返回两个参数,一个是 File 对象,可用于 I/O 操作,后文会详细介绍它,另外一个是 err,它是一个实现了 error interface 的结构体:

type PathError struct {
	Op   string
	Path string
	Err  error
}

其他获取 File 的方法

除上文所说的 OpenFile 函数可以获取 File 对象外,os 包还提供了 Open/Create/CreateTemp/NewFile 函数来获取 File,除 NewFile 外,其他函数都是 OpenFile 的再封装。其中,较为常用的是 Open 和 Create。

Open 用于以只读方式打开一个文件,其定义如下:

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

当我们想只读文件时,就可以使用 Open,后文在介绍读文件的时候,就会使用它来创建 File 对象。

Create 用于以读写方式打开一个文件,当文件不存在时创建它,且打开时会清空文件的内容:

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

因为 Create 在调用 OpenFile 时传入了 O_TRUNC——它会清空文件中已经存在的内容,所以我们想用 Create 来打开文件并追加内容是不可取的。

CreateTemp 用于创建一个临时文件,它创建的文件会在文件名末尾追加一个随机字符串,它的源码这里不在罗列,只列举一个使用案例:

// 会在当前目录创建一个 oldme + 随机字符串的文件
// 如果目录参数不传,则会把文件创建在不同的操作系统的临时目录中
func TestCreateTemp(t *testing.T) {
	_, err = os.CreateTemp("./", "oldme")
	if err != nil {
		t.Fatal(err)
	}
}

NewFile 会根据文件描述符创建一个新的文件对象,它并不依赖 OpenFile 函数,也不会真正在文件系统中创建一个文件,本文着重介绍文件的 I/O 操作,这里不在赘述它,有兴趣可以移步别处查看。

关闭 File

在每次获取打开文件使用完成后,应该调用 Close 关闭文件,防止文件描述符用尽:

func TestOpenFile(t *testing.T) {
	f, err := os.OpenFile("./demo.txt", os.O_TRUNC|os.O_WRONLY, 0777)
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
}

Close 会在如下两种情况下抛出错误:

  • 重复关闭文件
  • 关闭未打开文件

这两种情况都不应该出现在业务中,所以,我们无需处理 Close 的错误。

文件 I/O:读写操作

File 之读(一)

知晓怎么获取 File 对象后,我们就可以对获取的 File 对象进行读写操作了,先来看读,读文件依赖于这两个函数:ReadReadAt。Read 从当前文件偏移开始读,每读一次会改变一次文件偏移量,ReadAt 从指定位置开始读,读完后不会改变文件偏移量。所谓偏移量可以形象的看作鼠标指针,你鼠标指的地方就是文件偏移量,程序在读文件时,就会从鼠标指针这里读起。来看看这两个函数的具体使用:

Read:

// Read 会改变文件的偏移量
func TestRead(t *testing.T) {
	f, _ := os.Open("./demo.txt")
	defer f.Close()
	var b = make([]byte, 2)
	i, err := f.Read(b)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("第一次读取字节数:%d\n", i)
	t.Logf("读取的值:%s\n", string(b))
	i, err = f.Read(b)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("第二次读取字节数:%d\n", i)
	t.Logf("读取的值:%s\n", string(b))
}

demo.txt 里面的内容是 01234567

// 结果
第一次读取字节数:2
读取的值:01
第二次读取字节数:2
读取的值:23

Read 在第一次读取后,文件偏移量改变了,所以第二次读取就会从上次读取的位置开始。

ReadAt:

// ReadAt 从指定位置开始读,不会改变文件的偏移量
func TestReadAt(t *testing.T) {
	f, _ := os.Open("./demo.txt")
	defer f.Close()
	var b = make([]byte, 2)
	i, err := f.ReadAt(b, 2)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("第一次读取字节数:%d\n", i)
	t.Logf("读取的值:%s\n", string(b))
	i, err = f.Read(b)
	if err != nil {
		t.Fatal(err)
	}
	t.Logf("第二次读取字节数:%d\n", i)
	t.Logf("读取的值:%s\n", string(b))
}

demo.txt 里面的内容是 01234567

// 结果
第一次读取字节数:2
读取的值:23
第二次读取字节数:2
读取的值:01

ReadAt 在一次读取后,文件偏移量没有发生改变,所以 Read 还是从默认文件偏移量 0 开始读取。

当读取文件的内容是中文时,需要注意在中文在不同文件编码下所占据的字节数,如 UTF-8 是 3 个字节,GBK 是 2 个字节。

File 之读(二)

前文说过,Go 的 os 包功能设计类似于 Unix,所以也继承了 Unix 中一个重要的概念:”一切皆是文件“,那么,目录自然也是文件。所以,File 不仅是字面意思上,就单纯的指一个文件,它还有可能是一个目录,这就带来一个有趣的问题:如果对这个目录 File 使用 Read/ReadAt 会怎么样呢?

func TestFileIsDir(t *testing.T) {
	d, _ := os.Open("./")
	defer d.Close()
	var b = make([]byte, 2)
	_, err := d.Read(b)
	if err != nil {
		t.Fatal(err)
	}
}

// 结果
抛出异常:read ./: Incorrect function.

答案很明显,程序会抛出异常:Incorrect function。那么,既然目录也是文件,这个目录文件又怎么读呢?Go 为我们准备了三个函数:ReaddirnamesReadDirReaddir。Readdirnames 函数较为简单,就是获取该目录下所有的文件的文件名:

// 获取当前目录下所有文件的文件名(包括目录,因为目录也是文件)
func TestDirnames(t *testing.T) {
	d, _ := os.Open("./")
	defer d.Close()
	name, err := d.Readdirnames(0)
	if err != nil {
		return
	}
	for _, v := range name {
		fmt.Println(v)
	}
}

// 结果
base_test.go
demo.txt
get_file_test.go
read_file_test.go
test_sub

ReadDir 和 Readdir 这俩兄弟的命名也是相当的搞人,看上去差不多,但它们还是有着一些区别的:

func (f *File) ReadDir(n int) ([]DirEntry, error) // n 是需要获取的文件个数,如果小于1,则获取全部,下文同理
func (f *File) Readdir(n int) ([]FileInfo, error)

ReadDir 返回的类型是 []DirEntry,Readdir 返回的是 []FileInfo,聪明的你看到这个返回值的命名,想必就已经知道了它们的用法了,ReadDir 和 FileInfo 定义:

type DirEntry interface {
	Name() string // 返回文件名称
	IsDir() bool // 是否是目录
	Type() FileMode // 返回FileMode
	Info() (FileInfo, error) // 获取FileInfo
}

type FileInfo interface {
	Name() string       // 返回文件名称
	Size() int64        // 返回文件大小,单位字节
	Mode() FileMode     // 返回文件类型位,如:drwxrwxr--
	ModTime() time.Time // 返回修改时间
	IsDir() bool        // 是否是目录
	Sys() any           // 返回底层数据源
}​

 使用示例:

// 获取当前目录下所有的文件名
func TestReaddir(t *testing.T) {
	d, err := os.Open("./")
	dir, err := d.Readdir(0)
	if err != nil {
		return
	}
	for _, v := range dir {
		fmt.Printf("文件名: %s, isDir: %t\n", v.Name(), v.IsDir())
	}
}

// 结果
文件名: base_test.go, isDir: false
文件名: demo.txt, isDir: false
文件名: get_file_test.go, isDir: false
文件名: read_file_test.go, isDir: false
文件名: test_sub_dir, isDir: true

File 之写

文件写入有三个函数:Write、WriteAt、WriteString:

func (f *File) Write(b []byte) (n int, err error)
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
func (f *File) WriteString(s string) (n int, err error)

Write/WriteAt 和 Read/ReadAt 类似,WriteAt 指定文件偏移量,写完后也不会改变文件偏移量,WriteString 是对 Write 的再封装, 功能和 Write一样,只不过接收参数是 string 类型。在使用写入功能时,需要确保当前打开 File 的对象具有写入权限,否则会抛出权限不足的错误。使用示例:

func TestWrite(t *testing.T) {
	fw, _ := os.Create("./")
	defer fw.Close()

	w, err := fw.Write([]byte("012"))
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第一次写入了%d个字节\n", w)

	w, err = fw.Write([]byte("34"))
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第二次写入了%d个字节\n", w)
}
// 结果
第一次写入了3个字节
第二次写入了2个字节
// demo.txt
01234

func TestWriteAt(t *testing.T) {
	fw, _ := os.Create("./")
	defer fw.Close()

	w, err := fw.Write([]byte("01abc"))
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第一次写入了%d个字节\n", w)

	w, err = fw.WriteAt([]byte("234"), 2)
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第二次写入了%d个字节\n", w)
}
// 结果
第一次写入了5个字节
第二次写入了3个字节
// demo.txt
01234

func TestWriteString(t *testing.T) {
	fw, _ := os.Create("./")
	defer fw.Close()
	
	w, err := fw.WriteString("012")
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第一次写入了%d个字节\n", w)

	w, err = fw.WriteString("34")
	if err != nil {
		t.Fatal(err)
	}
	fmt.Printf("第二次写入了%d个字节\n", w)
}
// 结果
第一次写入了3个字节
第二次写入了2个字节
// demo.txt
01234

当对目录文件进行写入时,这三个函数会抛出一个错误:is a directory

文件若想追加写入,需要调整打开文件的方式:

// 追加写入
func TestWriteAppend(t *testing.T) {
	fw, _ := os.OpenFile("./demo.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0)
	_, err := fw.WriteString("abc")
	if err != nil {
		t.Fatal(err)
	}
}

文件的其他操作

创建目录

创建目录有两个主要函数:

func Mkdir(name string, perm FileMode) error
func MkdirAll(path string, perm FileMode) error // 区别于Mkdir,Mkdir可以创建多级目录,类似 [mkdir -p] 命令

使用示例:

func TestMkdir(t *testing.T) {
	_ = os.Mkdir("./sub", 0)
}

func TestMkAll(t *testing.T) {
	_ = os.Mkdir("./sub/sub2", 0)
}

删除文件(目录)

删除也有两个主要函数,和创建目录类似:

func Remove(name string) error
func RemoveAll(path string) error // 指定一个目录的话,会递归删除该目录下的所有文件

使用示例:

func TestRemove(t *testing.T) {
	_ = os.Remove("./demo.log")
}

func TestRemoveAll(t *testing.T) {
	_ = os.RemoveAll("./sub")
}

重命名和移动

重命名就是移动,这点和 Unix/Linux 一样:

func Rename(oldpath, newpath string) error

使用示例:

func TestRename(t *testing.T) {
	err := os.Rename("./demo.log", "./test_sub_dir/demo.log")
	if err != nil {
		t.Fatal(err)
	}
}

检查文件是否存在

Go os包判断文件是否存在比较奇特:不是根据一个路径直接来判断文件是否存在,而是根据 OpenFile/Open/Create 等函数的 Err 返回值来判断。

func IsNotExist(err error) bool // 文件不存在返回true,其他情况返回false

使用示例:

func TestIsNotExist(t *testing.T) {
	_, err := os.Open("./demo.log")
	b := os.IsNotExist(err)
	fmt.Println(b)
}

// 结果
demo.log不存在的话返回true

Os 包的其他功能

os 包除了提供文件操作外,还提供了一些其他的系统功能,如改变文件权限、执行系统命令、获取环境变量等,感兴趣的可以查看 Go os

评论

欢迎您的回复 取消回复

您的邮箱不会显示出来,*必填

本文目录