File文件操作

首先,file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现

FileInfo接口

FileInfo接口中定义了File信息相关的方法。

1
2
3
4
5
6
7
8
type FileInfo interface {
Name() string // base name of the file 文件名.扩展名 1.txt
Size() int64 // 文件大小,字节数 12540
Mode() FileMode // 文件权限 -rw-rw-rw-
ModTime() time.Time // 修改时间 2018-04-13 16:30:53 +0800 CST
IsDir() bool // 是否文件夹
Sys() interface{} // 基础数据源接口(can return 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
package main

import (
"fmt"
"os"
)

func main() {
fileInfo, err := os.Stat("main/1.md")
if err != nil {
fmt.Println("err :", err)
return
}
fmt.Printf("%T\n", fileInfo)
//文件名
fmt.Println(fileInfo.Name())
//文件大小
fmt.Println(fileInfo.Size())
//是否是目录
fmt.Println(fileInfo.IsDir()) //IsDirectory
//修改时间
fmt.Println(fileInfo.ModTime())
//权限
fmt.Println(fileInfo.Mode()) //-rw-r--r--
}

输出结果:

1
2
3
4
5
6
*os.fileStat
1.md
47
false
2023-06-10 21:30:41.7576415 +0800 CST
-rw-rw-rw-

权限

至于操作权限perm,除非创建文件时才需要指定,不需要创建新文件时可以将其设定为0。虽然go语言给perm权限设定了很多的常量,但是习惯上也可以直接使用数字,如0666(具体含义和Unix系统的一致)。

权限控制:

linux 下有2种文件权限表示方式,即“符号表示”和“八进制表示”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(1)符号表示方式:

- --- --- ---

type owner group others
文件的权限是这样子分配的 读 写 可执行 分别对应的是 r w x 如果没有那一个权限,用 - 代替
(-文件 d目录 |连接符号)
例如:-rwxr-xr-x

(2)八进制表示方式:
r ——> 004
w ——> 002
x ——> 001
- ——> 000

0755
0777(owner,group,others都是可读可写可执行)
0555
0444
0666

打开模式

文件打开模式:

1
2
3
4
5
6
7
8
9
10
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 // 如果可能,打开时清空文件
)

File操作

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
type File
//File代表一个打开的文件对象。

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

func Open(name string) (file *File, err error)
//Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。

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

func NewFile(fd uintptr, name string) *File
//NewFile使用给出的Unix文件描述符和名称创建一个文件。

func Pipe() (r *File, w *File, err error)
//Pipe返回一对关联的文件对象。从r的读取将返回写入w的数据。本函数会返回两个文件对象和可能的错误。

func (f *File) Name() string
//Name方法返回(提供给Open/Create等方法的)文件名称。

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

func (f *File) Fd() uintptr
//Fd返回与文件f对应的整数类型的Unix文件描述符。

func (f *File) Chdir() error
//Chdir将当前工作目录修改为f,f必须是一个目录。如果出错,错误底层类型是*PathError。

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

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

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

示例代码:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package main

import (
"fmt"
"os"
"path/filepath"
)

func main() {

//1.路径
fileName1 := "C:\\GolandProjects\\GoProject1\\main\\1.md"
fileName2 := "main/1.md"

//判断是否是绝对路径
fmt.Println(filepath.IsAbs(fileName1)) //true
fmt.Println(filepath.IsAbs(fileName2)) //false

//转化为绝对路径
//fmt.Println(filepath.Abs(fileName1))
//fmt.Println(filepath.Abs(fileName2)) // C:\GolandProjects\GoProject1\main\1.md <nil>

//1.获取目录
//fmt.Println("获取父目录:", filepath.Join(fileName1, ".."))
//fmt.Println("获取父目录:", filepath.Dir(fileName1))
//fmt.Println("获取当前目录:", filepath.Join(fileName1, "."))

//2.创建目录
err := os.Mkdir("main/app", os.ModePerm) //权限0777
if err != nil {
fmt.Println("err:", err)
return
}
fmt.Println("文件夹创建成功。。")

//err := os.MkdirAll("main/a/b/c", os.ModePerm)
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println("多层文件夹创建成功")

//3.创建文件:Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件已存在会截断它(为空文件)
//file1, err := os.Create(fileName1)
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println(file1)

//file2, err := os.Create(fileName2) //创建相对路径的文件,是以当前工程为参照的
//if err != nil {
// fmt.Println("err :", err)
// return
//}
//fmt.Println(file2)

//4.打开文件:
//file3, err := os.Open(fileName1) //只读的
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println(file3)
/*
第一个参数:文件名称
第二个参数:文件的打开方式
const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened.
)
第三个参数:文件的权限:文件不存在创建文件,需要指定权限
*/
//file4, err := os.OpenFile(fileName1, os.O_RDONLY|os.O_WRONLY, os.ModePerm)
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println(file4)

//5关闭文件,
//err := file4.Close()
//if err != nil {
// return
//}

//6.删除文件或文件夹:
//删除文件(该方法也可以删除空目录)
//err := os.Remove("main/1.md")
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println("删除文件成功。。")

//删除目录
//err := os.RemoveAll("main/a/b/c")
//if err != nil {
// fmt.Println("err:", err)
// return
//}
//fmt.Println("删除目录成功。。

文件读取

文件的操作函数和方法的介绍:

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
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) 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的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。

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

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

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

func (f *File) Write(b []byte) (n int, err error)
//Write向文件中写入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 (f *File) Seek(offset int64, whence int) (ret int64, err error)
//Seek设置下一次读/写的位置。offset为相对偏移量,而whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误。

func (f *File) Sync() (err error)
//Sync递交文件的当前内容进行稳定的存储。一般来说,这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存。

I/O操作

I/O操作也叫输入输出操作。其中I是指Input,O是指Output,用于读或者写数据的,有些语言中也叫流操作,是指数据通信的通道。

Golang 标准库对 IO 的抽象非常精巧,各个组件可以随意组合,可以作为接口设计的典范。

io包

io包中提供I/O原始操作的一系列接口。它主要包装了一些已有的实现,如 os 包中的那些,并将这些抽象成为实用性的功能和一些其他相关的接口。

由于这些接口和原始的操作以不同的实现包装了低级操作,客户不应假定它们对于并行执行是安全的。

在io包中最重要的是两个接口:Reader和Writer接口,首先来介绍这两个接口。

Reader接口的定义,Read()方法用于读取数据。

1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}

Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误。即使 Read 返回的 n < len(p),它也会在调用过程中使用 p的全部作为暂存空间。若一些数据可用但不到 len(p) 个字节,Read 会照例返回可用的东西,而不是等待更多。

当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF 情况,它就会返回读取的字节数。它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(和 n == 0)。这种一般情况的一个例子就是 Reader 在输入流结束时会返回一个非零的字节数,可能的返回不是 err == EOF 就是 err == nil。无论如何,下一个 Read 都应当返回 0, EOF。

调用者应当总在考虑到错误 err 前处理 n > 0 的字节。这样做可以在读取一些字节,以及允许的 EOF 行为后正确地处理I/O错误。

Read 的实现会阻止返回零字节的计数和一个 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package main

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

func main() {
/*
读取数据:
Reader接口:
Read(p []byte)(n int, error)
*/
//读取本地1.txt文件中的数据
//step1:打开文件
fileName := "main/1.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Println("err:", err)
return
}
//step3:关闭文件
defer func(file *os.File) {
err := file.Close()
if err != nil {

}
}(file)

//step2:读取数据
bs := make([]byte, 4, 4)
/*
//第一次读取
n, err := file.Read(bs)
fmt.Println(err) //<nil>
fmt.Println(n) //4
fmt.Println(bs) //[97 98 99 100]
fmt.Println(string(bs)) //abcd

//第二次读取
n, err = file.Read(bs)
fmt.Println(err) //<nil>
fmt.Println(n) //4
fmt.Println(bs) //[101 102 103 104]
fmt.Println(string(bs)) //efgh

//第三次读取
n, err = file.Read(bs)
fmt.Println(err) //<nil>
fmt.Println(n) //2
fmt.Println(bs) //[105 106 103 104]
fmt.Println(string(bs)) //ijgh

//第四次读取
n, err = file.Read(bs)
fmt.Println(err) //EOF,文件的末尾
fmt.Println(n) //0
*/
n := -1
for {
n, err = file.Read(bs)
if n == 0 || err == io.EOF {
fmt.Println("读取到了文件的末尾,结束读取操作。。")
break
}
fmt.Println(n)
fmt.Println(string(bs[:n]))
}
/*
abcd
efgh
ij
读取到了文件的末尾,结束读取操作。。
*/
}

Writer接口的定义,Write()方法用于写出数据。

1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

Write 将 len(p) 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数n(0 <= n <= len(p))以及任何遇到的引起写入提前停止的错误。若 Write 返回的n < len(p),它就必须返回一个非nil的错误。Write 不能修改此切片的数据,即便它是临时的。

示例代码:

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
package main

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

func main() {

fileName := "main/1.txt"
//step1:打开文件
//step2:写出数据
//step3:关闭文件
//file, err := os.Open(fileName)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()

//写出数据
//bs := []byte{65, 66, 67, 68, 69, 70} //A,B,C,D,E,F
//bs := []byte{97, 98, 99, 100} //a,b,c,d
//n,err := file.Write(bs)
//n, err := file.Write(bs[:2])
//fmt.Println(n)
//HandleErr(err)
//file.WriteString("\n")
//
//直接写出字符串
//n, err := file.WriteString("HelloWorld")
//fmt.Println(n)
//HandleErr(err)
////
file.WriteString("\n")
n, err := file.Write([]byte("today"))
fmt.Println(n)
HandleErr(err)

}
func HandleErr(err error) {
if err != nil {
log.Fatal(err)
}
}

Seeker接口的定义,封装了基本的 Seek 方法。

1
2
3
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}

Seeker 用来移动数据的读写指针
Seek 设置下一次读写操作的指针位置,每次的读写操作都是从指针位置开始的
whence 的含义:
如果 whence 为 0:表示从数据的开头开始移动指针
如果 whence 为 1:表示从数据的当前指针位置开始移动指针
如果 whence 为 2:表示从数据的尾部开始移动指针
offset 是指针移动的偏移量
返回移动后的指针位置和移动过程中遇到的任何错误

ReaderFrom接口的定义,封装了基本的 ReadFrom 方法。

1
2
3
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}

ReadFrom 从 r 中读取数据到对象的数据流中
直到 r 返回 EOF 或 r 出现读取错误为止
返回值 n 是读取的字节数
返回值 err 就是 r 的返回值 err

WriterTo接口的定义,封装了基本的 WriteTo 方法。

1
2
3
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}

WriterTo 将对象的数据流写入到 w 中
直到对象的数据流全部写入完毕或遇到写入错误为止
返回值 n 是写入的字节数
返回值 err 就是 w 的返回值 err

定义ReaderAt接口,ReaderAt 接口封装了基本的 ReadAt 方法

1
2
3
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}

ReadAt 从对象数据流的 off 处读出数据到 p 中
忽略数据的读写指针,从数据的起始位置偏移 off 处开始读取
如果对象的数据流只有部分可用,不足以填满 p
则 ReadAt 将等待所有数据可用之后,继续向 p 中写入
直到将 p 填满后再返回
在这点上 ReadAt 要比 Read 更严格
返回读取的字节数 n 和读取时遇到的错误
如果 n < len(p),则需要返回一个 err 值来说明
为什么没有将 p 填满(比如 EOF)
如果 n = len(p),而且对象的数据没有全部读完,则
err 将返回 nil
如果 n = len(p),而且对象的数据刚好全部读完,则
err 将返回 EOF 或者 nil(不确定)

定义WriterAt接口,WriterAt 接口封装了基本的 WriteAt 方法

1
2
3
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}

WriteAt 将 p 中的数据写入到对象数据流的 off 处
忽略数据的读写指针,从数据的起始位置偏移 off 处开始写入
返回写入的字节数和写入时遇到的错误
如果 n < len(p),则必须返回一个 err 值来说明
为什么没有将 p 完全写入

文件复制

在io包中主要是操作流的一些方法,今天主要学习一下copy。就是把一个文件复制到另一个目录下。

它的原理就是通过程序,从源文件读取文件中的数据,在写出到目标文件里。

io包下的Read()和Write()

我们可以通过io包下的Read()和Write()方法,边读边写,就能够实现文件的复制。这个方法是按块读取文件,块的大小也会影响到程序的性能。

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
package main

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

func main() {
srcFile := "main/1.txt"
destFile := "main/2.txt"
total, err := copyFile1(srcFile, destFile)
fmt.Println(total, err)
}

/*
该函数的功能:实现文件的拷贝,返回值是拷贝的总数量(字节),错误
*/
func copyFile1(srcFile, destFile string) (int, error) {
file1, err := os.Open(srcFile)
if err != nil {
return 0, err
}
file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return 0, err
}
defer file1.Close()
defer file2.Close()
// 拷贝数据
bs := make([]byte, 1024, 1024)
n := -1 //读取的数据量
total := 0
for {
n, err = file1.Read(bs)
if err == io.EOF || n == 0 {
fmt.Println("拷贝完毕。。")
break
} else if err != nil {
fmt.Println("报错了。。。")
return total, err
}
total += n
file2.Write(bs[:n])
}
return total, nil

}

io包下的Copy()

我们也可以直接使用io包下的Copy()方法。

示例代码如下:

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
package main

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

func main() {
srcFile := "main/1.txt"
destFile := "main/2.txt"
total, err := copyFile2(srcFile, destFile)
fmt.Println(total, err)
}

func copyFile2(srcFile, destFile string) (int64, error) {
file1, err := os.Open(srcFile)
if err != nil {
return 0, err
}
file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return 0, err
}
defer file1.Close()
defer file2.Close()

return io.Copy(file2, file1)
}

在io包中,不止提供了Copy()方法,还有另外2个公开的copy方法:CopyN(),CopyBuffer()。

1
2
3
4
5
Copy(dst,src) //为复制src 全部到 dst 中。

CopyN(dst,src,n) //为复制src 中 n 个字节到 dst。

CopyBuffer(dst,src,buf)//为指定一个buf缓存区,以这个大小完全复制。

无论是哪个copy方法最终都是由copyBuffer()这个私有方法实现的。

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
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}
if buf == nil {
size := 32 * 1024
if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
if l.N < 1 {
size = 1
} else {
size = int(l.N)
}
}
buf = make([]byte, size)
}
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = ErrShortWrite
break
}
}
if er != nil {
if er != EOF {
err = er
}
break
}
}
return written, err
}

从这部分代码可以看出,复制主要分为3种。

1.如果被复制的Reader(src)会尝试能否断言成writerTo,如果可以则直接调用下面的writerTo方法

2.如果 Writer(dst) 会尝试能否断言成ReadFrom ,如果可以则直接调用下面的readfrom方法

3.如果都木有实现,则调用底层read实现复制。

其中,有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
if buf == nil {
size := 32 * 1024
if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
if l.N < 1 {
size = 1
} else {
size = int(l.N)
}
}
buf = make([]byte, size)
}

这部分主要是实现了对Copy和CopyN的处理。通过上面的调用关系图,我们看出CopyN在调用后,会把Reader转成LimiteReader。

区别是如果Copy,直接建立一个缓存区默认大小为 32* 1024 的buf,如果是CopyN 会先判断 要复制的字节数,如果小于默认大小,会创建一个等于要复制字节数的buf。

ioutil包

第三种方法是使用ioutil包中的 ioutil.WriteFile()ioutil.ReadFile(),但由于使用一次性读取文件,再一次性写入文件的方式,所以该方法不适用于大文件,容易内存溢出。

示例代码:

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
package main

import (
"fmt"
"io/ioutil"
)

func main() {
srcFile := "main/1.txt"
destFile := "main/2.txt"
total, err := copyFile3(srcFile, destFile)
fmt.Println(total, err)
}

func copyFile3(srcFile, destFile string) (int, error) {
input, err := ioutil.ReadFile(srcFile)
if err != nil {
fmt.Println(err)
return 0, err
}

err = ioutil.WriteFile(destFile, input, 0644)
if err != nil {
fmt.Println("操作失败:", destFile)
fmt.Println(err)
return 0, err
}

return len(input), nil
}

目前ReadFile和WriteFile已弃用。

总结

最后,我们来测试一下这3种拷贝需要花费时间,拷贝的文件都是一样的一个mp4文件(400M)。

第一种:io包下Read()和Write()直接读写:我们自己创建读取数据的切片的大小,直接影响性能。

1
2
3
4
5
6
7
8
9
拷贝完毕。。
<nil>
401386819

real 0m7.911s
user 0m2.900s
sys 0m7.661s


第二种:io包下Copy()方法:

1
2
3
4
5
6
7
8
<nil>
401386819

real 0m1.594s
user 0m0.533s
sys 0m1.136s


第三种:ioutil包

1
2
3
4
5
6
7
<nil>
401386819

real 0m1.515s
user 0m0.339s
sys 0m0.625s

这3种方式,在性能上,不管是还是io.Copy()还是ioutil包,性能都是还不错的。