目录
前言
GO语言基础语法并不难,接下来开始就是GO语言的重点,核心编程部分将介绍Go语言中文件,通道,goroutine,反射,单元测试等内容。本章将介绍GO语言对文件的一些基本操作。
一、文件信息的获取
一个文件包含着许多信息,我们往往需要关注的信息包括文件名,文件大小,文件最后一次的修改时间等等。GO语言中提供了一个FileInfo接口,我们可以通过这个接口获得文件的各种信息,通过os.Stat()可以获取一个文件的FileInfo。
/* 1. 通过os.stat()获取文件信息,获取fileInfo接口,有以下方法:
1. Name() 获取文件名
2. Size() 获取文件大小,以字节为单位
3. Mode() 获取文件的权限位.如-rw-rw-r--
4. ModTIme() 获取文件的最新修改时间
5. IsDir() 判断当前文件是否为一个目录
6. Sys() 一些底层系统的数据
*/
// 使用os.Stat()返回一个fileInfo接口
// 若不存在该文件,os.Stat()不会创建该文件,而是会报错: no such file or directory
func getFileInfo(fileName string){ // 此处输入"a.txt"
fi,err := os.Stat(fileName)
if err != nil{
fmt.Printf("get fileInfo failed,err:%v\n",err)
return
}
fmt.Println(fi.Name()) // 输出 a.txt
fmt.Println(fi.Size()) // 输出 0
fmt.Println(fi.Mode()) // 输出 -rw--rw-r--
fmt.Println(fi.IsDir())// 输出 false
}
我们还可以使用filepath中的相关函数,对文件路径进行一些相关操作。
// 使用filepath可以对文件路径进行相关操作
// 1.Abs()获取当前文件路径的绝对路径
// 2.IsAbs(),IsRel()判断当前路径是否是绝对路径
// 3.Join(),获取固定层级的路径
func dealFIlePath(fileName string){
absPath,err := filepath.Abs(fileName)
if err != nil{
fmt.Printf("get abs path failed,err:%v\n",err)
return
}
fmt.Println(absPath)
fmt.Println(filepath.IsAbs(absPath))
fmt.Println(filepath.Join(absPath,"..")) //获取当前文件绝对路径的上一级路径,可在后面加n个".."表示上n级路径
}
二、文件的基本操作
2.1.创建文件
Go语言包提供了创建文件的方法,创建文件又分为创建目录和创建文件,有以下方法
//创建一个文件或者目录
//创建目录
os.Mkdir(name string,perm FileMode)error,若目录已经存在,会报错: file exists
err := os.Mkdir("a",os.ModePerm) // os.ModePerm默认0777
if err != nil{
fmt.Printf("make dir failed,err:%v\n",err)
return
}
getFileInfo("./a")
//创建多级嵌套目录,使用os.MkdirAll()
err := os.MkdirAll("./b/c/d",os.ModePerm)
if err != nil{
fmt.Printf("make dir failed,err:%v\n",err)
return
}
//创建一个文件
os.Create(name string)(file *File,err error)创建一个名为name的文件,权限为0666,如果文件已存在会进行覆盖
f,err := os.Create("b.txt")
if err != nil{
fmt.Printf("create file failed,err:%v\n",err)
return
}
defer f.Close()
注意:养成良好习惯,每创建或打开一个文件,都要在后面紧跟一句defer将该文件关闭,以防IO被占用浪费资源!!!
2.2.读取文件
GO语言提供了多种读取文件的方法,包括io中*File接口进行读写,该读写方法直接与IO交互,因此速度会慢一些,第二种方法是带缓冲区的读,即bufio包提供的读方法,该方法会建立一个缓冲区,先将部分内容读进缓冲区,用户需要读文件时直接从缓冲区读,速度明显提升。还有一个方法,通过ioutil包直接读取文件,该方法一次性将整个文件读取下来,因此只适合读较小的文件。下面分别介绍三种方法。
2.2.1. IO接口直接读
func readFile1(fileName string) {
// 对文件进行读写操作,无论是读还是写,都要先打开文件,读写完后一定要关闭文件,否则会占用IO
// 读文件
f, err := os.Open(fileName) // os.Open()以只读形式打开文件
if err != nil {
fmt.Printf("open file failed,err:%v\n", err)
return
}
defer f.Close()
buf := make([]byte, 4)
for {
n, err := f.Read(buf)
if err == io.EOF { // f.Read()读到文件末尾会返回io.EOF错误
fmt.Printf("read file finish\n")
break
}
if err != nil {
fmt.Printf("read file failed,err:%v\n", err)
break
}
fmt.Printf("read some contents:%v\n", string(buf[:n]))
}
}
2.2.2. 带缓冲区的读(bufio)
func readFile2(fileName string) {
f, err := os.Open(fileName) // os.Open()以只读形式打开文件
if err != nil {
fmt.Printf("open file failed,err:%v\n", err)
return
}
defer f.Close()
reader := bufio.NewReader(f)
//str1,err := reader.ReadString('\n') //reader.ReadString(delim byte) 读取文件直到遇到delim字符,会把读取到的内容连带delim字符返回
//if err != nil{
// fmt.Printf("read string failed,err:%v\n",err)
// return
//}
//fmt.Printf("%v",str1)
//
//line,_,err:=reader.ReadLine() // 读取一行内容,返回值的第二个元素isPrefix,如果该行太大超过了缓冲区,该值就会被置为true
//if err != nil{
// fmt.Printf("read line failed,err:%v\n",err)
// return
//}
//fmt.Println(string(line))
for {
str1, err := reader.ReadString('\n') //reader.ReadString(delim byte) 读取文件直到遇到delim字符,会把读取到的内容连带delim字符返回
if err == io.EOF {
fmt.Printf("read file finish\n")
break
}
if err != nil {
fmt.Printf("read string failed,err:%v\n", err)
return
}
fmt.Printf("read string:%v", str1) //输出abcdefghi,hhhh,读不到最后一行的nihao,因为最后一行没有回车
}
}
bufio包中reader接口提供了许多种读文件的方法,如ReadString(),ReadLine(),ReadByte()等,每种方法的输入与返回值都各有不同,需要多加练习。
常用的是ReadString(delim byte),该函数读取字符串直到遇到delim字符,会将字符串连带该字符返回给输出。但这样用来读文件会遇到一个问题,就是当文件最后一行如果没有以delim字符结尾的话(如示例中的'\n') ,那么最后一行读到IO.EOF就会退出,最后一行将无法读到。实际上内容有读取在缓冲区,如果我们想要最后一行,可以在处理io.EOF时将其读出。
2.2.3. ioutil包直接读
func readFile3(fileName string) {
msg, err := ioutil.ReadFile(fileName)
if err != nil { // ioutil读取文件,读完之后不会返回io.EOF错误
fmt.Printf("read file failed,err:%v\n", err)
return
}
fmt.Printf("read msg:%v\n", string(msg))
}
2.3.写入文件
与读文件一样,写文件也可以使用三种方法来写,但是要注意,写文件时要保证文件是以可写的模式被打开的,因此要使用os.OpenFile()函数来打开文件,该函数提供不同的模式位来以不同方式打开文件。
// 定义
func OpenFile(name string,flag int,perm FileMode)(*File,error)
/*
输入参数:
name:文件名
flag:标志位
O_RDONLY //只读
O_WRONLY //只写
O_RDWR //读写
O_APPEND //追加
O_CREATE //创建
O_EXCL //判断文件是否已经存在,与os.O_CREATE一起使用,如果文件存在就会报错
O_SYNC
O_TRUNC //清空
perm:权限,一般写0777
*/
2.3.1. IO接口直接写
func writeFile1(fileName string) {
f, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0777)
if err != nil {
fmt.Printf("open file failed,err:%v\n", err)
return
}
defer f.Close()
msg := "I am function writeFile1\n"
_, err = f.Write([]byte(msg))
if err != nil {
fmt.Printf("write file failed,err:%v\n", err)
return
}
}
2.3.2. 带缓冲区的写(bufio)
func writeFile2(fileName string) {
f, err := os.OpenFile(fileName, os.O_RDWR|os.O_APPEND, 0777)
if err != nil {
fmt.Printf("open file failed,err:%v\n", err)
return
}
defer f.Close()
writer := bufio.NewWriter(f)
msg := "I am function writeFile2"
_, err = writer.WriteString(msg)
if err != nil {
fmt.Printf("write file failed,err:%v\n", err)
return
}
writer.Flush() // writer接口所有方法都是将内容写到缓冲区,必须使用Flash()方法将缓冲区内容刷新到文件中
}
bufio的writer接口也提供了许多写文件的方法,可以参考标准库go语言标准库中文文档 多加练习。
注意,使用bufio进行写文件时,当写的内容大小小于缓冲区时,内容是保存在缓冲区中的,如果要其写入文件中,必须使用Flush()方法刷新到文件中。
2.3.3. ioutil包直接写
func writeFile3(fileName string) {
msg := "I am function writeFile3"
err := ioutil.WriteFile(fileName, []byte(msg), 0777) //如果文件已存在会覆盖
if err != nil {
fmt.Printf("write file failed,err:%verr\n", err)
return
}
}
2.4. 文件拷贝
func stdCopy(src,dst string)(int64,error){
srcFile,err := os.Open(src)
if err != nil{
return -1,err
}
dstFile,err := os.OpenFile(dst,os.O_RDWR|os.O_CREATE,0777)
if err != nil{
return -1,err
}
defer srcFile.Close()
defer dstFile.Close()
n,err := io.Copy(dstFile,srcFile) //io.Copy()直接拷贝文件
if err != nil{
return -1,err
}
return n,nil
}
2.5.文件光标置位
有时候我们不希望读写文件的时候总是从文件的开始读起,File接口给我们提供了一个Seek()方法,可以通过Seek()方法将光标通过迁移量放置在需要的位置。
// 定义
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
/*
参数:
offset:偏移量,以字节为单位
whence:偏移量的起点,有以下三种选项:
SeekStart 文件首
SeekCurrent 光标当前所在位置
SeekEnd 文件末尾
*/
代码示例如下:
//6.指定读写位置
f, err := os.OpenFile("./a.txt", os.O_RDWR, 0777)
if err != nil {
fmt.Printf("open file failed,err:%v\n", err)
return
}
defer f.Close()
f.Seek(4, io.SeekStart) // 将光标置位,位于起点后第四个字之后
f.WriteString("hhh")
2.6.删除文件
// 7.删除文件或目录
os.Remove("./c.txt") // 删除单个目录或文件
os.RemoveAll("./b/") // 删除所有目录或文件
三、练习
3.1. 使用三种不同的方式实现文件拷贝
下面会使用三种不同的方式对一个图片(1.36M)进行拷贝,使用相同大小的Buffer,比较三种不同方式的读写效率。
- 使用io.File接口进行拷贝
// 1.使用io的读写方法复制文件, func ioCopy(src,dst string)(int,error){ begin := time.Now() // 获取当前时间 srcFile,err := os.Open(src) // 打开源文件 if err != nil{ return -1,err } dstFile,err := os.OpenFile(dst,os.O_RDWR|os.O_CREATE,0777) //打开目标文件 if err != nil{ return -1,err } defer srcFile.Close() //函数返回时关闭源文件 defer dstFile.Close() //函数返回时关闭目标文件 Buf := make([]byte,1024) //定义一个大小为1024字节的buf用于读写文件 n := -1 // n用于记录每次读写的字节数 total := 0 //记录总的读取字节数,即拷贝了多少字节 for{ n,err = srcFile.Read(Buf) // 每次从源文件读取1024字节 if err == io.EOF || n == 0{ //读到文件尾退出 fmt.Printf("file copy finish\n") break } if err != nil{ return -1,err } n,err = dstFile.Write(Buf[:n]) // 往目标文件写读到的内容 if err != nil{ return -1,err } total += n //记录拷贝了多少字节 } end := time.Now() // 再次获取当前时间 fmt.Println(end.Sub(begin)) //就算拷贝花了多少时间 return total,nil } func main(){ srcFileName := "./girl.jpg" temp := strings.Split(srcFileName[strings.LastIndex(srcFileName, "/")+1:], ".") dstFileName := string(temp[0]) + ".bak2." + string(temp[1]) //目标文件名 = girl.bak.jpg n, err := ioCopy(srcFileName, dstFileName) if err != nil { fmt.Printf("copy failed,err:%v\n", err) return } fmt.Println(n) /*输出: file copy finish 47.261092ms 1431529 */ }
可以看到,使用io接口直接拷贝一份1.36M的文件需要耗时47ms,直接操作IO耗时太大。
- 使用bufio进行拷贝
// 2.bufio包实现复制 // 代码看起来相似,但是底层原理不同,bufio带了缓冲区,不直接操作io,速度快了好几倍 func bufioCopy(src, dst string) (int, error) { begin := time.Now() // 获取拷贝开始时间 srcFile, err := os.Open(src) // 打开源文件 if err != nil { return -1, err } dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0777) //打开目标文件 if err != nil { return -1, err } defer srcFile.Close() //函数返回时关闭源文件 defer dstFile.Close() //函数返回时关闭目标文件 reader := bufio.NewReader(srcFile) // 创建一个reader接口 writer := bufio.NewWriter(dstFile) // 创建一个writer接口 Buf := make([]byte, 1024) //定义一个1024字节的buf用于文件读写 n := -1 // n记录每次读写到的字节数 total := 0 //total记录拷贝的总字节数 for { n, err = reader.Read(Buf) // 读取源文件 if err == io.EOF || n == 0 { // 读到文件末尾退出 fmt.Printf("file copy finish\n") break } if err != nil { return -1, err } n, err = writer.Write(Buf[:n]) //写文件 if err != nil { return -1, err } total += n } end := time.Now() // 获取拷贝结束时的时间 fmt.Println(end.Sub(begin)) // 计算拷贝总时间 return total, nil } func main(){ srcFileName := "./girl.jpg" temp := strings.Split(srcFileName[strings.LastIndex(srcFileName, "/")+1:], ".") dstFileName := string(temp[0]) + ".bak2." + string(temp[1]) //目标文件名 = girl.bak.jpg n, err := bufioCopy(srcFileName, dstFileName) if err != nil { fmt.Printf("copy failed,err:%v\n", err) return } fmt.Println(n) /*输出: file copy finish 5.455529ms 1431529 */ }
可以明显看到,使用bufio通过缓冲区对文件进行读写,同样大小(1.36M)的文件,拷贝时间只需要5.45ms,足足快了8.6倍!
因此,bufio是最推荐使用的读写文件方式,它通过缓冲区减少了访问IO的次数,大大提高了文件的读写效率。- 使用ioutil进行拷贝
// 3.通过ioutil包实现文件拷贝 func ioutilCopy(src, dst string) (int, error) { begin := time.Now() //获取文件拷贝开始时时间 data, err := ioutil.ReadFile(src) // 直接读取源文件 if err != nil { return -1, err } err = ioutil.WriteFile(dst, data, 0777) // 将内容写到目标文件 if err != nil { return -1, err } end := time.Now() // 获取文件拷贝结束时的时间 fmt.Println(end.Sub(begin)) // 打印文件拷贝花费的总时间 return len(data), nil } func main(){ srcFileName := "./girl.jpg" temp := strings.Split(srcFileName[strings.LastIndex(srcFileName, "/")+1:], ".") dstFileName := string(temp[0]) + ".bak2." + string(temp[1]) //目标文件名 = girl.bak.jpg n, err := ioutilCopy(srcFileName, dstFileName) if err != nil { fmt.Printf("copy failed,err:%v\n", err) return } fmt.Println(n) /*输出: 4.632226ms 1431529 */ }
可以看到,使用ioutil读写文件时,当文件很小的时候,速度优势是很大的。因此,iouti;包适用于小文件读写。
- 使用ioutil进行拷贝
3.2. 断点续传
所谓断点续传,就是在传送或拷贝文件时,可能因为一些不可抗力因素导致了程序突然终止。这时候我们如果重启程序,让程序从头传起可以说是十分浪费时间和资源的一件事,因此我们要设计一个函数让程序在重启之和能接着上一次的终点继续传输或拷贝文件。
- 思路:
- 开启一个临时文件,这个临时文件存储着当前已经传输或拷贝的字节数
- 每次传输或拷贝文件前,先到临时文件中读取已经传输或拷贝的字节数
- 设置源文件与目标文件的光标位置,接着上一次的终点读写
- 当传输或拷贝完文件后,将临时文件删除
- 下面提供一个断点复制文件的代码实现
func breakCopy(src, dst string) (int64, error) { srcFile, err := os.Open(src) // 打开源文件 if err != nil { return -1, err } dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0777) //打开目标文件 if err != nil { return -1, err } tempFileName := src[strings.LastIndex(src, "/")+1:] + ".temp" tempFile, err := os.OpenFile(tempFileName, os.O_RDWR|os.O_CREATE, 0777) //打开临时文件 if err != nil { return -1, err } defer srcFile.Close() //函数返回时关闭源文件 defer dstFile.Close() //函数返回时关闭目标文件 count := int64(0) // count用于记录临时文件中存储的内容(即已经拷贝的字节数) data := make([]byte, 1024) // 该切片用于读取源文件的内容和写入目标文件 tempBuf := make([]byte, 100) // 该切片用于写入临时文件 n, err := tempFile.Read(tempBuf) // 读取临时文件 if n == 0 || err == io.EOF { // 这层判断是第一次阅读临时文件时,文件中啥都没有,会返回io.EOF count = 0 } else if err != nil { return -1, err } else { count, err = strconv.ParseInt(string(tempBuf[:n]), 10, 64) // 获取已经拷贝的字节数 if err != nil { return -1, err } } fmt.Printf("count:%v\n", count) total := count // 总字节数 srcFile.Seek(count, io.SeekStart) //源文件置位光标 dstFile.Seek(count, io.SeekStart) // 目标文件置位光标 for { n, err = srcFile.Read(data) //读取源文件 if err == io.EOF || n == 0 { tempFile.Close() // 读完关闭并删除临时文件 os.Remove(tempFileName) break } n, err = dstFile.Write(data[0:n]) //写入目标文件 if err != nil { return -1, err } total = total + int64(n) // 记录总拷贝字节数 tempFile.Seek(0, io.SeekStart) //置位临时文件光标,从头写起 _, err = tempFile.WriteString(strconv.Itoa(int(total))) //写入已经拷贝的字节数 if err != nil { return -1, err } fmt.Printf("total:%v\n", total) //这里模拟程序断电 //if total > 8000{ // panic("断电了。。。") //} } return total, nil }
3.3. 遍历一个文件夹下的所有子目录与子文件
// 练习3:遍历一个目录下的所有文件
func ergodicAllFile(path string, level int) {
s := "|--"
for i := 0; i < level; i++ { // 分层级,让输出有层次感
s = "| " + s
}
fileInfos, err := ioutil.ReadDir(path) //该函数返回一个[]FileInfo切片,包含了该目录下所有文件的信息
if err != nil {
fmt.Printf("read file failed,err:%v\n", err)
return
}
for _, fi := range fileInfos {
fileName := path + "/" + fi.Name()
fmt.Printf("%v%v\n", s, fileName)
if fi.IsDir() { // 如果是目录,则递归输出子目录子文件
ergodicAllFile(fileName, level+1)
}
}
}
总结
本章介绍了Go语言中对文件的基本操作,我们可以通过获取FileInfo接口来获取文件的各种信息,通过os.Stat()获取FileInfo,可以通过filepath包对文件路径进行相关处理。打开文件我们有两种方法,其中os.Open()默认只读方式打开,os.OpenFile()可通过不同模式位改变文件的读写模式。对于文件的读写,我们有三种方法,一种是使用io文件接口直接读写,该方法直接操作IO,故速度比较慢,第二种是带缓冲区的读写,该方法将读写内容先读写到缓冲区,再与IO进行交互,减少了与IO交互的次数,因此读写速度显著提升,最后一种通过ioutil包对整个文件进行读写,这中方法适合读写较小的文件。我们还可以通过Seek()方法置位文件的光标,从而改变读写位置。
生命不息,coding不止!