文件模式

Go语言的文件模式和 0777 权限常量定义在 os 包(事实上在io/fs包)中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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权限位(用于通过&获取类型位)
)

Go语言定义了 FileInfo 接口来描述文件信息,FileInfo 接口定义如下:

1
2
3
4
5
6
7
8
type FileInfo interface {
Name() string // 文件的名字(不含扩展名)
Size() int64 // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同
Mode() FileMode // 文件的模式位
ModTime() time.Time // 文件的修改时间
IsDir() bool // 等价于Mode().IsDir()
Sys() interface{} // 底层数据来源(可以返回nil)
}
  • func Stat(name string) (fi FileInfo, err error) 函数获取文件名name的 FileInfo 信息。如果出错,返回的错误值为*PathError类型。
  • func (f *File) Stat() (fi FileInfo, err error) 用于获取打开的文件的 FileInfo 信息。
  • func (f *File) Name() string 可直接返回已打开文件的文件名。
1
2
3
4
5
6
7
8
func main() {
// 获取文件信息
fi, _ := os.Stat("./src")
fmt.Printf("src stat: %+v\n", fi)
}

// 结果
src stat: &{name:src FileAttributes:16 CreationTime:{LowDateTime:4178585970 HighDateTime:30952027} LastAccessTime:{LowDateTime:440928839 HighDateTime:30952057} LastWriteTime:{LowDateTime:1431422461 HighDateTime:30952056} FileSizeHigh:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:D:\GoPram\src\demo\Learnfile\src vol:0 idxhi:0 idxlo:0 appendNameToPath:false}

文件操作失败的错误类型

Go语言为因路径问题导致的失败定义了 PathError 类型,PathError 记录一个错误,以及导致错误的路径。

1
2
3
4
5
6
7
type PathError struct {
Op string
Path string
Err error
}

func (e *PathError) Error() string

对于链接操作和重命名操作定义了 LinkError 类型,LinkError 记录在 Link、Symlink、Rename 系统调用时出现的错误,以及导致错误的路径。

1
2
3
4
5
6
7
8
type LinkError struct {
Op string
Old string
New string
Err error
}

func (e *LinkError) Error() string

创建文件夹

使用 Mkdir(name string, perm FileMode) 函数根据指定的目录名 name 和权限 perm 创建一个目录,如果出错,会返回 *PathError 类型的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 以 0666(读写权限)创建一个名为 temp 的目录
package main

import (
"fmt"
"log"
"os"
)

func main() {
err := os.Mkdir("temp", 0666)
if err != nil {
log.Println("创建目录 temp 失败, err:", err.Error())
} else {
fmt.Println("创建目录 temp 成功")
}
}

// 执行结果如下
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
创建目录 temp 成功
PS D:\GoPram\src\demo\Learnfile> ls

Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/4/7 16:32 temp
-a---- 2022/4/7 16:32 242 dir_file.go
-a---- 2022/4/7 16:30 4081 os.go

使用 Mkdir(name string, perm FileInfo) 只能在当前工作目录下创建新目录,若需要递归创建多层目录需要使用 MkdirAll(path string, perm FileInfo) 函数。权限位perm会应用在每一个被本函数创建的目录上。如果path指定了一个已经存在的目录,MkdirAll不做任何操作并返回nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
err := os.MkdirAll("./src/susu/wei", os.ModePerm)
if err != nil {
log.Println("创建目录 ./src/susu/wei 失败, err:", err.Error())
} else {
fmt.Println("创建目录 ./src/susu/wei 成功")
}
}

// 执行结果如下
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
创建目录 ./src/susu/wei 成功
PS D:\GoPram\src\demo\Learnfile> cd ./src/susu
PS D:\GoPram\src\demo\Learnfile\src\susu> ls

Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2022/4/7 16:46 wei

打开文件的flag

在操作文件之前先介绍下操作文件的模式(OpenFile flag)即指定以什么样的方式操作文件(如只读,只写,读写,追加写…)

Go语言在 os 包定义了如下部分flag常量

1
2
3
4
5
6
7
8
9
10
11
12
const (
// 必须指定 O_RDONLY, O_WRONLY, or O_RDWR 中的一个.
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 // 打开时截断文件,即文件有内容会被丢弃,并设置文件大小为0.
)

多个控制行为可通过按位或运算 | 来添加

1
2
// 以读写模式打开文件,写文件时在文件末尾追加(保留文件已有的内容)
OpenFile("suwei.go", os.O_RDWR | O_APPEND, 0)

创建文件

没有专门用于创建文件的函数,可以使用 OpenFile(name string, flag int, perm FileInfo) 函数,调用时传递 O_CREATE flag 打开一个不存在的 name 文件,便会以 perm 权限创建 name文件,并返回此文件的文件指针和nil,如果出错,会返回 *PathError 类型的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 创建 suwei.go
suwei, err := os.OpenFile("./src/susu/suwei.go", os.O_RDONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("创建./src/susu/suwei.go失败, err:", err.Error())
} else {
// 打印文件的信息
mesg, _ := suwei.Stat()
fmt.Printf("%+v", mesg)
}
}

// 输出
&{name:suwei.go FileAttributes:32 CreationTime:{LowDateTime:2659761341 HighDateTime:30952037} LastAccessTime:{LowDateTime:2659761341 HighDateTime:30952037} LastWriteTime:{LowDateTime:2659761341 HighDateTime:30952037} FileSizeHigh:0 FileSizeLow:0 Reserved0:0 filetype:1 Mutex:{state:0 sema:0} path: vol:3366446870 idxhi:589824 idxlo:4173 appendNameToPath:false}

多数时候创建文件都使用 0666(读写)权限,因此,Go语言封装了一个 Create(name string) 函数,用于简化创建 0666 权限的文件。func Create 的源码如下:

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

注:标志位中添加了 O_TRUNC 因此对于已经存在的文件,本函数会将其截断。

打开文件

上一节已经介绍了 func OpenFile 函数,可使用该函数打开文件。对于只使用只读模式打开文件Go语言封装了 Open(name string) 函数。 func Open 源码如下:

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

关闭文件

文件使用完后应当关闭,习惯上把关闭资源的语句放在defer语句处理。关闭文件的函数申明 func (*os.File).Close() error

1
defer srcF.Close()

删除文件

使用 Remove(name string) 删除name指定的文件或目录。如果出错,会返回*PathError底层类型的错误。

1
2
3
4
5
6
7
8
9
func main() {
// 删除文件或目录
err := os.Remove("./src/susu/suwei.go")
if err != nil {
log.Println("删除失败, err:", err.Error())
} else {
fmt.Println("删除成功")
}
}

使用 Remove 函数只能删除空目录,如果需要递归删除目录需要使用 RemoveAll(path string) 函数。如果path指定的对象不存在,RemoveAll会返回nil而不返回错误。

1
2
3
4
5
6
7
8
9
func main() {
// 删除非空目录
err := os.RemoveAll("./src/susu")
if err != nil {
log.Println("删除失败, err:", err.Error())
} else {
fmt.Println("删除成功")
}
}

重命名文件(移动文件)

使用 Rename(oldpath, newpath string) 对文件重命名或移动文件

1
2
3
4
5
6
7
8
9
func main() {
// 重命名或移动文件(目录)
err := os.Rename("./temp", "./newTemp")
if err != nil {
fmt.Println("重命名失败")
} else {
fmt.Println("重命名成功")
}
}

从打开的文件中读取数据

从头读取

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
// 读取文件
srcFile, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}
// 读完后关闭文件
defer srcFile.Close()

// 创建一个 []byte 来接收文件的数据
dataT := make([]byte, 1024)

n, err := srcFile.Read(dataT)
if err != nil {
fmt.Println("文件读取失败")
} else {
fmt.Printf("文件已读取完, 共读取: %d个字节\n", n)
fmt.Println(string(dataT))
}
}

// 结果
文件已读取完, 共读取: 63个字节
毫无疑问,我做的馅饼,是全天下,最好吃的。

注:当读取0个字节时返回值err才可能为io.EOF,如果是文件字节数少于切片长度,一次性读完了,返回值err为nil,不为io.EOF,因为n不等于0

循环读取大文件

注:每读取一次,下一次读/写的位置会向后偏移n个字节,n为本次读取到的字节数

基于此,对于读取大文件可以采用循环读取,直到返回值err为io.EOF 或其他错误时结束。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 使用循环读取大文件
func main() {
srcFile, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}
// 读完后关闭文件
defer srcFile.Close()

// 创建一个 []byte 来接收文件的数据
dataT := make([]byte, 4)
dst := make([]byte, 100)

for {
n, err := srcFile.Read(dataT)
if err != nil {
if err == io.EOF {
fi, _ := srcFile.Stat()
fmt.Printf("文件%q读取完毕, 文件大小: %dB\n", fi.Name(), fi.Size())
} else {
fmt.Println("文件读取失败")
}
break
} else {
dst = append(dst, dataT[:n]...)
}
}

fmt.Println(string(dst))
}

// 结果
文件"su.txt"读取完毕, 文件大小: 63B
毫无疑问,我做的馅饼,是全天下,最好吃的。

从其他位置开始读取

使用ReadAt

使用 Read 方法只会从文件开始处读取文件,若不从文件开始处读取,可以使用 func ReadAt(b []byte, off int64) (n int, err error) 从指定位置 off 开始读取,即先从文件开始处偏移 off 个字节后再读取。当n<len(b)时,本方法总是会返回错误;如果是因为到达文件结尾,返回值err会是io.EOF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
// ReadAt 从文件开始处偏移 n 个字节再读取
srcFile, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}
// 读完后关闭文件
defer srcFile.Close()

// 创建一个 []byte 来接收文件的数据
dataT := make([]byte, 1024)

n, err := srcFile.ReadAt(dataT, 15)
if err == io.EOF {
fmt.Printf("文件已读取完, 共读取: %d个字节\n", n)
fmt.Println(string(dataT))
} else {
fmt.Println("文件读取失败")
}
}

// 结果
文件已读取完, 共读取: 48个字节
我做的馅饼,是全天下,最好吃的。

注:当n<len(b)时,本方法总是会返回错误,如果文件字节数少于切片长度,一次性读完了,返回值err为io.EOF,不为nil。这和Read 方法有所区别。

使用Seek手动偏移

除了使用 ReadAt 方法外,还可以使用 func (f *File) Seek(offset int64, whence int) (ret int64, err error) 手动偏移后再读取,offset为偏移量,whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误。os 包中定义了相应的常量。

1
2
3
4
5
const (
SEEK_SET int = 0 // 相对文件开始位置偏移
SEEK_CUR int = 1 // 相对文件当前位置偏移
SEEK_END int = 2 // 相对文件结尾位置偏移
)

使用 Seek 改写 ReadAt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
// Seek手动偏移
srcFile, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}
// 读完后关闭文件
defer srcFile.Close()

// 创建一个 []byte 来接收文件的数据
dataT := make([]byte, 1024)

p, _ := srcFile.Seek(15, os.SEEK_SET)
fmt.Println("新的偏移量:", p)
n, err := srcFile.Read(dataT)
if err != nil {
fmt.Println("文件读取失败")
} else {
fmt.Printf("文件已读取完, 共读取: %d个字节\n", n)
fmt.Println(string(dataT))
}
}

// 结果
新的偏移量: 15 // 新的偏移量指的是从头开始到当前的读写位置有多少个字节
文件已读取完, 共读取: 48个字节
我做的馅饼,是全天下,最好吃的。

两个新增的读文件函数/方法

  • func ReadFile(name string) ([]byte, error) 1.16 引入

    一次性读完文件,成功的调用返回值err == nil 而不是 err == EOF ,因为是一次性读完,所以不会将 EOF 视为要报告的错误。

  • func (f *File) ReadFrom(r io.Reader) (n int64, err error) 1.15 引入

    接收一个 io.Reader 接口类型,将 r 中的数据 读入 到文件 f 中,返回读入的字节数和可能的错误。

往文件写

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

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

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

  • func WriteFile(name string, data []byte, perm FileMode) error 1.16 引入

    如果是往新文件(或者覆盖)写入数据,需要先新建文件,再写入。而本函数就是以上两个过程的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
// 写文件
text := "suwei susu ddv."
dstFile, err := os.Create("./src/suwei.txt")
if err != nil {
fmt.Println("创建文件失败")
}
defer dstFile.Close()

n, err := dstFile.WriteString(text)
if err != nil {
fmt.Println("写文件出错, err:", err.Error())
} else {
fmt.Println("写文件成功,写入字节:", n)
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
写文件成功,写入字节: 15
PS D:\GoPram\src\demo\Learnfile> cat src/suwei.txt
suwei susu ddv.

拷贝文件

由于 os 包没有提供直接拷贝文件的函数,所以需要自己将源文件的内容读出来,写入到目标文件来实现拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func main() {
// 拷贝文件
srcFi, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}

defer srcFi.Close()

dstFi, err := os.Create("./src/dest.txt")
if err != nil {
fmt.Println("创建文件失败, err:", err.Error())
}

defer dstFi.Close()

// 创建一个[]byte切片中转
transit := make([]byte, 1024*4)

for {
n, err := srcFi.Read(transit)
if err != nil {
if err == io.EOF {
fmt.Println("拷贝完成")
} else {
fmt.Println("读取出错, err:", err.Error())
}
break
}
_, err = dstFi.Write(transit[:n])
if err != nil {
fmt.Println("写入失败, err:", err.Error())
}
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
拷贝完成
PS D:\GoPram\src\demo\Learnfile> cat src/dest.txt
毫无疑问,我做的馅饼,是全天下,最好吃的。

使用 io.Copy 函数

因为 io 包下的 func Copy(dst Writer, src Reader) (written int64, err error) 函数实现了将 src 的数据拷贝到 dst 中,直到遇到EOF或第一个错误。返回拷贝的字节数和遇到的第一个错误。对成功的调用,返回值err为nil而非EOF,因为Copy定义为从src读取直到EOF,它不会将读取到EOF视为应报告的错误。如果src实现了WriterTo接口,本函数会调用src.WriteTo(dst)进行拷贝;否则如果dst实现了ReaderFrom接口,本函数会调用dst.ReadFrom(src)进行拷贝。

并且 *os.File 类型实现了 Reader、Writer 接口,因此可以使用 io.Copy 函数替换 for 循环的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
// 借助io.Copy函数拷贝文件
srcFi, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败, err:", err.Error())
}

defer srcFi.Close()

dstFi, err := os.Create("./src/dest.txt")
if err != nil {
fmt.Println("创建文件失败, err:", err.Error())
}

defer dstFi.Close()

if n, err := io.Copy(dstFi, srcFi); err != nil {
fmt.Println("拷贝出错, err:", err.Error())
} else {
fmt.Printf("拷贝完成, 拷贝数据: %dB", n)
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
拷贝完成, 拷贝数据: 63B

修改文件模式

  • func Chmod(name string, mode FileMode) error 将文件名为 name 的文件模式修改为 mode。如果name指定的文件是一个符号链接,它会修改该链接的目的地文件的mode。如果出错,会返回*PathError底层类型的错误。
  • func (f *File) Chmod(mode FileMode) error 用于修改已打开的文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 修改文件模式
err := os.Chmod("./src/dest.txt", os.ModePerm)
if err != nil {
fmt.Println("修改文件模式失败")
} else {
fmt.Println("修改文件模式成功")
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
修改文件模式成功

修改文件大小(截断文件)

  • func Truncate(name string, size int64) error 将name文件的大小修改为size个字节。如果该文件为一个符号链接,将修改链接指向的文件的大小。如果出错,会返回*PathError底层类型的错误。
  • func (f *File) Truncate(size int64) error 用于修改已打开的文件的大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 修改文件大小
err := os.Truncate("./src/dest.txt", 15)
if err != nil {
fmt.Println("修改大小失败")
return
}
if fi, err := os.Open("./src/dest.txt"); err != nil {
fmt.Println("打开文件失败")
} else {
finfo, _ := fi.Stat()
fmt.Printf("文件名: %s\t大小: %dB\n", finfo.Name(), finfo.Size())
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
文件名: dest.txt 大小: 15B

判断是否为目录

使用 func (m FileMode) IsDir() bool 判断当前的文件模式是否为一个目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 判断是否为目录
fi, err := os.Open("./newTemp")
if err != nil {
fmt.Println("打开文件失败")
return
}
info, _ := fi.Stat()
if info.Mode().IsDir() {
fmt.Println("./newTemp 是目录")
} else {
fmt.Println("./newTemp 是普通文件")
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
./newTemp 是目录

判断是否为普通文件

使用 func (m FileMode) IsRegular() bool 判断当前文件模式是否为普通文件,与 IsDir 相对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
// 判断是否为普通文件
fi, err := os.Open("./newTemp/su.txt")
if err != nil {
fmt.Println("打开文件失败")
return
}
info, _ := fi.Stat()
if info.Mode().IsRegular() {
fmt.Println("./newTemp/su.txt 是文件")
} else {
fmt.Println("./newTemp/su.txt 是目录")
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
./newTemp/su.txt 是文件

判断文件或目录是否存在

  • func IsExist(err error) bool 判断目录或文件是否存在。根据err错误判断是否为 ErrExist 常量。
  • func IsNotExist(err error) bool 与上一个相反。

错误类型常量定义在 oserror 包中:

1
2
3
4
5
6
7
8
9
10
11
package oserror

import "errors"

var (
ErrInvalid = errors.New("invalid argument")
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
)

判断两个文件是否一样

使用 func SameFile(fi1, fi2 FileInfo) bool 可以通过比较两个文件的 FileInfo 判断两个文件是否一致。

读取目录

  • func (f *File) Readdir(n int) (fi []FileInfo, err error)

    Readdir读取目录f的内容,返回一个有n个成员的[]FileInfo,这些FileInfo是被Lstat返回的,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。

    如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。

    如果n<=0,Readdir函数返回目录中剩余所有文件对象的FileInfo构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的FileInfo构成的切片和该错误。

  • func (f *File) ReadDir(n int) ([]DirEntry, error) 1.16 引入

    和小写的 Readdir 几乎一致,不同的是只返回目录条目的切片,不会包含普通文件。

    其中 DirEntry 定义在 fs 包:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    type DirEntry interface {
    // Name returns the name of the file (or subdirectory) described by the entry.
    // This name is only the final element of the path (the base name), not the entire path.
    // For example, Name would return "hello.go" not "home/gopher/hello.go".
    Name() string

    // IsDir reports whether the entry describes a directory.
    IsDir() bool

    // Type returns the type bits for the entry.
    // The type bits are a subset of the usual FileMode bits, those returned by the FileMode.Type method.
    Type() FileMode

    // Info returns the FileInfo for the file or subdirectory described by the entry.
    // The returned FileInfo may be from the time of the original directory read
    // or from the time of the call to Info. If the file has been removed or renamed
    // since the directory read, Info may return an error satisfying errors.Is(err, ErrNotExist).
    // If the entry denotes a symbolic link, Info reports the information about the link itself,
    // not the link's target.
    Info() (FileInfo, error)
    }
  • func ReadDir(name string) ([]DirEntry, error) 1.16 引入

    内部调用了 f.ReadDir(-1) ,省去了打开文件。

  • func (f *File) Readdirnames(n int) (names []string, err error)

    Readdir读取目录f的内容,返回一个有n个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。

    如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。

    如果n<=0,Readdir函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func main() {
// 读取目录
file, err := os.Open("./src")
if err != nil {
fmt.Println("打开文件失败")
return
}
defer file.Close()

subfiles, err := file.Readdir(0)
if err != nil {
fmt.Println("读取目录出错")
return
}

fmt.Println("./src")
for _, f := range subfiles {
var sizeStr string
size := f.Size()
if size > 1000_000 {
sizeStr = strconv.FormatFloat(float64(size)/1000_000, 'f', 2, 64) + "MB"
} else if size > 1000 {
sizeStr = strconv.FormatFloat(float64(size)/1000, 'f', 2, 64) + "KB"
} else {
sizeStr = strconv.FormatInt(size, 10) + "B"
}
fmt.Printf(" %s\t%s\tsize: %s\n", f.Mode(), f.Name(), sizeStr)
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\dir_file.go
./src
-rw-rw-rw- dest.txt size: 15B
-rw-rw-rw- suwei.txt size: 15B
-rw-rw-rw- sxr-dest.jpg size: 4.45MB

完结:递归拷贝文件

之前实现的拷贝文件方法只能拷贝单个文件,对于拷贝整个目录是更为普遍的需要。下面实现一个递归拷贝文件的函数作为结尾。

要想递归拷贝,重点就是区分目录和文件,以使用不同的方式进行处理,可以使用 func (fs.FileMode).IsRegular() bool 方法判断是否为普通文件。对于普通文件我们直接拷贝;对于目录我们新建目录,再使用 func (*os.File).Readdir(n int) ([]fs.FileInfo, error) 方法读取该目录,递归处理读取到的每一个子条目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 拷贝函数,可以递归拷贝
func Copy(srcPath, dstPath string) error {
srcF, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcF.Close()

info, err := srcF.Stat()
if err != nil {
return err
}
var currFile = dstPath + "/" + info.Name()

// 普通文件,直接拷贝
if info.Mode().IsRegular() {
dstf, err := os.Create(currFile)
if err != nil {
return err
}
defer dstf.Close()

_, err = io.Copy(dstf, srcF)
if err != nil {
os.Remove(currFile)
}
return err
}

// 是个目录,递归处理
err = os.Mkdir(currFile, os.ModePerm)
if err != nil {
os.RemoveAll(currFile)
return err
}
subFile, err := srcF.Readdir(-1)
if err != nil {
return err
}
for _, sub := range subFile {
if err = Copy(srcPath+"/"+sub.Name(), currFile); err != nil {
if err != nil {
os.RemoveAll(currFile)
}
return err
}
}

return nil
}

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
// 实现递归拷贝目录
if err := Copy("./newTemp", "./src"); err != nil {
fmt.Println("拷贝失败, err:", err.Error())
} else {
fmt.Println("拷贝完成")
}

// 拷贝一个文件到当前目录
if err := Copy("./newTemp/sxr.jpg", "./"); err != nil {
fmt.Println("拷贝失败, err:", err.Error())
} else {
fmt.Println("拷贝完成")
}
}

// 结果
PS D:\GoPram\src\demo\Learnfile> go run .\copy.go
拷贝完成
拷贝完成