小抄系列进阶篇涉及的概念较多,知识点重要,故每块知识点独立成篇,方便日后笔记的查询
文件操作是必须要点满的技能,太重要了,开发必定会用到,学就完事
本篇的主题是:文件操作
文件
go语言的文件类(结构体+方法)定义在os包中,在其中封装了底层的文件描述符、文件相关信息和读写文件的方法
如下工程目录下除了示例代码文件外,还有用于测试文件操作的test.txt文件
.
├── file1.go
└── test.txt
#text.txt内容:
goland是世界上最好的语言!
获取文件信息
go语言定义了一个名为FileInfo
接口,对外提供了获取文件相关信息的方法
// A FileInfo describes a file and is returned by Stat and Lstat.
type FileInfo interface {
Name() string // 文件名.拓展名
Size() int64 // 文件大小
Mode() FileMode // 文件权限 rwxrwxrwx r->可读 w->可写 x->可执行 从左至右三个一组,分别为文件拥有者 组 其他用户
ModTime() time.Time // 文件最后一次修改时间
IsDir() bool // 是否为文件夹
Sys() interface{} // 基础数据源接口 (can return nil)
}
ps:文件权限可以用八进制表示:
r -> 004
w -> 002
x -> 001
- -> 000
go语言给权限设定了很多常量,但是习惯上可以使用二进制数值表示文件的权限如0777
有了接口,那么如何获取实现接口的文件对象呢?
go语言对外暴露的两个获取FileInfo类型对象的方法
-
func Stat
func Stat(name string) (fi FileInfo, err error)
Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为*PathError类型
-
func Lstat
func Lstat(name string) (fi FileInfo, err error)
Lstat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接的信息,本函数不会试图跳转该链接。如果出错,返回的错误值为*PathError类型
代码实操:
package main
import (
"fmt"
"os"
)
func main() {
/*
通过 FileInfo接口 获取文件信息
*/
//1.获取文件对象
fileInfo, err := os.Stat("/home/GoWorkSpace/src/fileTest/test.txt")
//fileInfo,err := os.Stat("/home/GoWorkSpace/src/fileTest/test1.txt") //不存在的文件
if err != nil {
fmt.Println(err) //stat /home/GoWorkSpace/src/fileTest/test1.txt: no such file or directory
return
}
fmt.Printf("%T\n", fileInfo) //*os.fileStat
//2.获取文件名
fileName := fileInfo.Name()
fmt.Printf("%T, %v\n", fileName, fileName) //string, test.txt
//3.获取文件大小
fmt.Printf("%T, %v\n", fileInfo.Size(), fileInfo.Size()) //int64, 36
//4.判断是否为文件夹
fmt.Printf("%T, %v\n", fileInfo.IsDir(), fileInfo.IsDir()) //bool, false
//5.获取最后修改时间
fmt.Printf("%T, %v\n", fileInfo.ModTime(), fileInfo.ModTime()) //bool, false
//6.获取文件读写权限
fmt.Printf("%T, %v\n", fileInfo.Mode(), fileInfo.Mode()) //os.FileMode, -rw-r--r--
}
文件路径
获取文件路径的信息和操作定义在path和path/filepath包中
package main
import (
"fmt"
"path"
"path/filepath"
)
func main() {
/*
路径:
相对路径:relative
text.txt 相对于当前工程
绝对路径:absolute
/home/GoWorkSpace/src/fileTest/test1.txt
. 当前路径
.. 上一层
*/
//1.判断是为绝对路径
relative := "text.txt" //相对路径
absolute := "/home/GoWorkSpace/src/fileTest/test1.txt" //绝对路径
fmt.Println(filepath.IsAbs(relative)) //false
fmt.Println(filepath.IsAbs(absolute)) //true
//2.获取文件绝对路径(主要是提供相对路径获取绝对路径,绝对路径获取绝对路径意义不大)
fmt.Println(filepath.Abs(relative)) // /home/GoWorkSpace/src/fileTest/text.txt <nil>
//3.获取文件的父目录 下面两种方式都可以
var parentDir = path.Join(absolute, "..")
fmt.Println(parentDir) ///home/GoWorkSpace/src/fileTest
fmt.Println(path.Dir(absolute)) // /home/GoWorkSpace/src/fileTest
fmt.Println(path.Dir(path.Dir(absolute))) // /home/GoWorkSpace/src
//4.获取文件名
fmt.Println(path.Base(absolute)) //test1.txt
}
File操作
操作文件或目录的函数也定义在os包下
创建文件/文件夹
package main
import (
"fmt"
"os"
)
func main() {
/*
1.创建文件夹:
如果文件存在则创建失败
os.Mkdir() 创建一层文件
os.MkdirAll() 创建多层文件夹
2.创建文件
Create采用666模式(任何人可读写但不可执行),创建一个指定名称的文件,如果文件一存在会覆盖它(为空文件)
os.Create() 创建文件,可用相对路径也可以用绝对路径
ps:注意:Create创建的文件返回的文件对象对应的文件描述符为O_RDONLY模式,即只能进行读操作
*/
//1.创建目录
//1.1、os.Mkdir() 需要传入两个参数:1.创建的文件夹完全名 2.文件夹权限
err1 := os.Mkdir("/home/GoWorkSpace/src/fileTest/dir", os.ModePerm) //os.ModePerm常量为777
if err1 != nil {
fmt.Println(err1) //重复执行报错:mkdir /home/GoWorkSpace/src/fileTest/dir: file exists
} else {
fmt.Println("创建成功")
} //ps:Mkdir只能创建一层文件夹
//1.2、当想连续创建多层子目录的时候,则需要使用os.MkdirAll()方法,传入参数和上面一致
err2 := os.MkdirAll("/home/GoWorkSpace/src/fileTest/dir1/dir2/dir", os.ModePerm)
if err2 != nil {
fmt.Println(err2) //重复执行报错:mkdir /home/GoWorkSpace/src/fileTest/dir: file exists
} else {
fmt.Println("递归创建我目录成功")
}
//2.创建文件
//Create采用666模式(任何人可读写但不可执行),创建一个指定名称的文件,如果文件一存在会覆盖它(为空文件)
file1, err3 := os.Create("/home/GoWorkSpace/src/fileTest/test2.txt")
if err3 != nil {
fmt.Println(err3) //如果创建的文件路径不存在则报错
//创建已经存在的同名文件,会覆盖掉旧的文件
} else {
fmt.Printf("%T\n", file1) //*os.File 返回的是文件对象指针
fmt.Println(file1) //&{0xc000050180}
}
//也可以用相对路径创建文件(相对与当前工程)
file2, err4 := os.Create("test3.txt")
if err4 != nil {
fmt.Println(err4)
} else {
fmt.Printf("%T\n", file2) //*os.File 返回的是文件对象指针
fmt.Println(file2) //&{0xc0000501e0}
}
//ps:注意:Create创建的文件返回的文件对象对应的文件描述符为O_RDONLY模式,即只能进行读操作
}
删除文件/文件夹
示例代码延续上面创建的文件和目录
package main
import (
"fmt"
"os"
)
func main() {
/*
删除文件或文件夹
os.Remove() 删除空目录或文件
os.RemoveAll() 删除所有
*/
//os.Remove既可以删除文件,也可以删除目录
//删除文件
err1 := os.Remove("/home/GoWorkSpace/src/fileTest/test2.txt") //删除一个文件
if err1 != nil {
//重复删除报错
fmt.Println(err1) //remove /home/GoWorkSpace/src/fileTest/test2.txt: no such file or directory
} else {
fmt.Println("文件删除成功")
}
//删除空目录
err2 := os.Remove("/home/GoWorkSpace/src/fileTest/dir")
//err2 := os.Remove("/home/GoWorkSpace/src/fileTest/dir1")
if err2 != nil {
//如果 1.目录里面有文件或文件夹 2.目录不存在 则报错
fmt.Println(err2) //remove /home/GoWorkSpace/src/fileTest/dir1: directory not empty
} else {
fmt.Println("目录删除成功")
}
//递归删除目录和目录下所有东西
err3 := os.RemoveAll("/home/GoWorkSpace/src/fileTest/dir1")
if err3 != nil {
fmt.Println(err3)
} else {
fmt.Println("递归删除目录成功")
}
}
打开文件
打开文件实际就是上当前程序和指定的文件之间建立一个连接,使得程序可以去愉快的"玩弄"这个文件,例如读写
因为是一个连接,所以要记住,骚操作完后要去关闭连接,节省资源,避免内存泄漏
学习过别的语言的文件操作应该很熟悉,打开一个文件是要提供对应的打开模式的(是只写、只读、可读可写还是追加等等)
对于打开文件的模式,go语言提供了一组常量供我们使用(源码注解还是挺见名知意的,就不全部翻译了):
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 // 和O_CREATE搭配使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O.
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件.
)
还是通过代码示例,看下如果打开文件
package main
import (
"fmt"
"os"
)
func main() {
/*
1.打开文件:程序和指定文件建立连接
os.Open():打开文件,只有读的权限(文件描述符为有O_RDONLY模式)
os.OpenFile():指定模式打开文件,需要传入打开文件的模式,可随即应对所有情况
第一个参数:文件名
第二个参数:文件的打开方式
第三个参数:文件的权限,文件不存在则创建文件,该文件需要赋予权限
2.关闭文件:程序和文件之间断开连接
file.Close()
*/
//1.打开文件
file1, err1 := os.Open("test.txt") //可以写相对路径也可以使用绝对路径
if err1 != nil {
fmt.Println(err1)
} else {
fmt.Printf("%T\n", file1) //*os.File 返回的文件对象指针,只有写权限
fmt.Println(file1) //&{0xc000104120}
}
//指定模式打开文件
file2, err2 := os.OpenFile("test.txt", os.O_RDONLY|os.O_WRONLY, os.ModePerm) //读写模式打开文件,后面的权限参数主要用于文件不存在时,创建文件后赋予的权限使用
if err2 != nil {
fmt.Println(err2)
} else {
fmt.Printf("%T\n", file2) //*os.File 返回的文件对象指针,只有写权限
fmt.Println(file2) //&{0xc000104180}
}
//2.关闭文件:打开文件操作完后记得要去关闭文件,可以通过defer函数统一去关闭所有的文件对象,避免内存泄漏
defer file1.Close()
defer file2.Close()
}
文件读写
读写文件即磁盘I/O操作,首先需要认识下什么是I/O
I/O操作
I/O操作即输入输出操作。用于读写数据
输入和输出是相对来说的,如果以程序读写文件或远程网络来说(主体是程序),读取文件或从远程网络获取数据,就是数据输入的过程;如果数据在程序产生要用于持久化到文件或者上传到网络上,就是数据输出的过程
在某些语言中,I/O操作也叫做流操作,指的是数据通信的通道,可以把流理解成是对程序与外界交换数据的一种抽象
go语言的io操作的相关api定义在io包中
io包中只是定义了一系列I/O操作的接口和封装了一些底层实现,具体设备的io操作实现分别定义与各自的包中,如文件的I/O操作(磁盘I/O)即文件的读写操作具体实现就定义在os包下
在io包中,其中有两个最为重要的接口:Reader和Writer接口
Reader接口
Reader接口定义如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
接口中只定义了Read()方法,其用于读取数据
-
参数
p
是byte类型的切片,Read方法读取到的数据就存储到p
当中 -
返回值
n
表示读取数据的字节数(0<=n<=len(p)) -
当Read遇到错误时或 EOF(到达末尾),返回的
err!=nil
,n=0
-
os包下的File结构就提供了I/O操作,其实现了Reader方法,用于读取文件
Writer接口
Writer接口定义如下:
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方法的类型都实现了 io.Writer 接口
Seeker接口
接口定义如下:
type Seeker interface {
Seek(offset int64, whence int) (ret int64, err error)
}
官方文档中关于该接口方法的说明:
Seek 设置下一次 Read 或 Write 的偏移量为 offset,它的解释取决于 whence: 0 表示相对于文件的起始处,1 表示相对于当前的偏移,而 2 表示相对于其结尾处。 Seek 返回新的偏移量和一个错误,如果有的话。
whence 的值,在 io 包中定义了相应的常量,应该使用这些常量
const (
SeekStart = 0 // seek relative to the origin of the file
SeekCurrent = 1 // seek relative to the current offset
SeekEnd = 2 // seek relative to the end
)
读取文件
读取文件是通常通过File类实现的Reader接口中的Read方法而实现,其定义如下:
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
下面通过示例演示文件的读操作:
有工程目录下有test.txt
文件,在其中写着goland是世界上最好的语言!
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
/*
读取文件:
Read接口:
Read(p []byte) (n int, err error)
*/
//读取本地test.txt的数据
//step1:打开文件(获取File对象)
fileName := "/home/GoWorkSpace/src/fileTest/test.txt"
file, err := os.Open(fileName) //和本地文件建立连接,可以读写数据了
if err != nil{
fmt.Println(err)
return
}
//step3:关闭文件(这第三步在go语言中一般提前使用defer去执行)
defer file.Close() //断开连接
//step2:读取数据
bs := make([]byte,24,24) //Read方法读取到的数据存储在bs字节切片中
//第一次读取
n, err2 := file.Read(bs) //n为读取到的字符数,err2为返回的错误
fmt.Printf("读取的字符数:%d\n",n)
fmt.Println("错误信息为",err2)
fmt.Println("读取到的数据为:",bs)
fmt.Printf("读取到的数据为:%s\n",bs) //以utf8格式现实字符 也可以使用string(bs)进行强转
/*
输出:
读取的字符数:24
错误信息为 <nil>
读取到的数据为: [103 111 108 97 110 100 230 152 175 228 184 150 231 149 140 228 184 138 230 156 128 229 165 189]
读取到的数据为:goland是世界上最好
*/
//第二次读取
n, err3 := file.Read(bs)
fmt.Printf("读取的字符数:%d\n",n)
fmt.Println("错误信息为",err3)
fmt.Println("读取到的数据为:",bs)
fmt.Printf("读取到的数据为:%s\n",bs) //以utf8格式现实字符 也可以使用string(bs)进行强转
/*
输出:
读取的字符数:12
错误信息为 <nil>
读取到的数据为: [231 154 132 232 175 173 232 168 128 239 188 129 231 149 140 228 184 138 230 156 128 229 165 189]
读取到的数据为:的语言!界上最好
光标继上一次读取到位置继续往下读取内容
取实际上读取到 "的语言!" 后,文件已经读取完毕了,后面的字符是之前上一次读取留下的
即再次读取,如果还有数据未读完,会重新从字节切片的开头填充内容
*/
//第三此读取
n, err4 := file.Read(bs)
fmt.Printf("读取的字符数:%d\n",n)
fmt.Println("错误信息为",err4)
fmt.Println("读取到的数据为:",bs)
fmt.Println("读取到的数据为:",string(bs))
/*
输出:
读取的字符数:0
错误信息为 EOF
读取到的数据为: [231 154 132 232 175 173 232 168 128 239 188 129 231 149 140 228 184 138 230 156 128 229 165 189]
读取到的数据为:的语言!界上最好
上一次已经读取完所以内容,再次调用读取方法,err返回EOF,读取的字符数为0
*/
//平时写也不会那么呆瓜一次一次的自己去读取,而是通过循环,判断err是否到达末尾作为终止条件
//下面是一般的标准写法
println("--------------------------------")
file2, err := os.Open(fileName)
defer file2.Close()
bs2 := make([]byte,1024,1024) //字节数组作为读取缓冲区,一般设置为1024
n1 := -1 //返回读取的数量,输出化为-1
for {
n1,err = file2.Read(bs2)
if n1 == 0 || err == io.EOF {
fmt.Println("读取到了文件的末尾,结束读取操作")
break
}
fmt.Println(string(bs2[:n1])) //打印的数据应该是读取了多少打印了多少
}
/*
输出:
goland是世界上最好的语言!
读取到了文件的末尾,结束读取操作
*/
//除了Read方法读取,还有ReaderAt和ReaderFrom
//ReaderAt方法用于指定位置去读取(Reader是从头开始读)
fmt.Println(file.ReadAt(bs,2)) //从第二个字节(不是字符)位置开始读
fmt.Println(string(bs)) //land是世界上最好�
//ReaderFrom() 从io对象读取数据
//下面的例子简单的实现将文件中的数据全部读取(显示在标准输出):
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()
/*
ReadFrom 从 r 中读取数据,直到 EOF 或发生错误。
其返回值 n 为读取的字节数。除 io.EOF 之外,在读取过程中遇到的任何错误也将被返回。
我们可以通过 ioutil 包的 ReadFile 函数获取文件全部内容。
其实,跟踪一下 ioutil.ReadFile 的源码,会发现其实也是通过 ReadFrom 方法实现(用的是 bytes.Buffer,它实现了 ReaderFrom 接口)
如果不通过 ReadFrom 接口来做这件事,而是使用 io.Reader 接口,我们有两种思路:
1.先获取文件的大小(File 的 Stat 方法),之后定义一个该大小的 []byte,通过 Read 一次性读取
2.定义一个小的 []byte,不断的调用 Read 方法直到遇到 EOF,将所有读取到的 []byte 连接到一起
*/
}
写文件
读取文件通常是通过File类实现的Writer接口中的Write方法而实现,其定义如下:
// Write writes len(b) bytes to the File.
// It returns the number of bytes written and an error, if any.
// Write returns a non-nil error when n != len(b).
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
epipecheck(f, e)
if e != nil {
err = f.wrapErr("write", e)
}
return n, err
}
还是通过代码示例直观演示:
在工程目录下创建writeFile.txt
文件
package main
import (
"fmt"
"os"
)
func main() {
/*
写文件
*/
fileName := "/home/GoWorkSpace/src/fileTest/writeFile.txt" //所要写文件的fullName
//step1:打开文件
//file, err := os.Open(fileName) //Open只能通过只读的方式打开文件,不能实现文件不存在时创建文件
file, err := os.OpenFile(fileName,os.O_RDWR|os.O_CREATE,os.ModePerm)
if err != nil{
fmt.Println(err)
return
}
//step3:关闭文件
defer file.Close()
//step2:写数据
//写数据,会覆盖原有的数据,因为我们是通过O_RDWR模式打开的文件,如果想追加末尾写数据需要使用O_APPEND模式打开
bs := []byte{'J','o','c','h','e','n','\n'} //byte<->uint8
n,err := file.Write(bs)
fmt.Printf("写进文件%d个字节\n",n)
fmt.Println(err)
//获取文件的字节数量,创建相应大小的缓冲区,一次性读取
info,_ := file.Stat()
rbs := make([]byte,info.Size(),info.Size())
rn, rerr := file.ReadAt(rbs,0) //写入数据光标会停留在文件末尾,所以使用ReadAt方法指定光标位置读取
fmt.Println("读取的字节数为:",rn)
fmt.Println(rerr)
fmt.Println("读取的数据为:",string(rbs))
//写文件除了Write方法
//1.还可以使用StringWriter接口定义的WriteString方法该方法可以直接写入字符串,不用搞上面的字节切片那么麻烦
n2, err2 := file.WriteString("写的好爽啊哈哈哈\n")
fmt.Println(n2)
fmt.Println(err2)
//用write写直接写字符串也可以,因为字符串是可以转成字节切片的
n3, err3 := file.Write([]byte("写的也还算舒服\n"))
fmt.Println(n3)
fmt.Println(err3)
//2.还可以使用WriteAt方法,作用和ReadAt一毛一样,就是指定光标位置去写
n4, err4 := file.WriteAt([]byte("指定位置写\n"),0) //重头开始写
fmt.Println(n4)
fmt.Println(err4)
}
本系列学习资料参考:
https://www.bilibili.com/video/BV1jJ411c7s3?p=15
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter01/01.1.html