File文件操作 首先,file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现
FileInfo接口 FileInfo接口中定义了File信息相关的方法。
1 2 3 4 5 6 7 8 type FileInfo interface { Name() string Size() int64 Mode() FileMode ModTime() time.Time IsDir() bool Sys() interface {} }
示例代码:
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 mainimport ( "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()) fmt.Println(fileInfo.ModTime()) fmt.Println(fileInfo.Mode()) }
输出结果:
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_SYNC int = syscall.O_SYNC 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 Filefunc Create (name string ) (file *File, err error )func Open (name string ) (file *File, err error )func OpenFile (name string , flag int , perm FileMode) (file *File, err error )func NewFile (fd uintptr , name string ) *Filefunc Pipe () (r *File, w *File, err error )func (f *File) Name() string func (f *File) Stat() (fi FileInfo, err error )func (f *File) Fd() uintptr func (f *File) Chdir() error func (f *File) Chmod(mode FileMode) error func (f *File) Chown(uid, gid int ) error func (f *File) Close() 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 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 mainimport ( "fmt" "os" "path/filepath" ) func main () { fileName1 := "C:\\GolandProjects\\GoProject1\\main\\1.md" fileName2 := "main/1.md" fmt.Println(filepath.IsAbs(fileName1)) fmt.Println(filepath.IsAbs(fileName2)) err := os.Mkdir("main/app" , os.ModePerm) 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 )func (f *File) Readdirnames(n int ) (names []string , err error )func (f *File) Truncate(size int64 ) error func (f *File) Read(b []byte ) (n int , err error )func (f *File) ReadAt(b []byte , off int64 ) (n int , err error )func (f *File) Write(b []byte ) (n int , err error )func (f *File) WriteString(s string ) (ret int , err error )func (f *File) WriteAt(b []byte , off int64 ) (n int , err error )func (f *File) Seek(offset int64 , whence int ) (ret int64 , err error )func (f *File) Sync() (err error )
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 mainimport ( "fmt" "io" "os" ) func main () { fileName := "main/1.txt" file, err := os.Open(fileName) if err != nil { fmt.Println("err:" , err) return } defer func (file *os.File) { err := file.Close() if err != nil { } }(file) bs := make ([]byte , 4 , 4 ) 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])) } }
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 mainimport ( "fmt" "log" "os" ) func main () { fileName := "main/1.txt" 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() 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 mainimport ( "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 mainimport ( "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) CopyN(dst,src,n) CopyBuffer(dst,src,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 wt, ok := src.(WriterTo); ok { return wt.WriteTo(dst) } 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 mainimport ( "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包,性能都是还不错的。