Go os 包介绍与使用
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 对象进行读写操作了,先来看读,读文件依赖于这两个函数:Read
和 ReadAt
。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 为我们准备了三个函数:Readdirnames
、ReadDir
、Readdir
。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 。
本文目录