本文章为笔者对于Go语言文件操作知识的粗浅认知,如有不当,烦请指正
Go语言中关于文件操作一般为以下几种:
这些操作有一些地方是一样的,类似于模板,是相对固定的代码,比方说我们都是先打开文件才能进行读写,而打开文件之后一定要关闭,否则会造成过度消耗资源,异常安全等问题,说清楚这个之后,下面我们来详细学习
首先我们来说明一下文件路径,很简单但很重要
在Go语言中读取文件时,文件的路径是一个字符串,它指定了文件在文件系统中的位置。这个路径可以是相对路径或绝对路径。
相对路径是相对于当前工作目录的路径。例如,如果你的程序位于目录 /home/user/myapp
,并且你想读取同一目录下的 config.txt
文件,你可以使用相对路径 ./config.txt
。
绝对路径是从文件系统的根目录开始的完整路径。使用绝对路径可以确保无论当前工作目录在哪里,都能正确找到文件。例如,/home/user/myapp/config.txt
是一个绝对路径。
我们使用os.Open()函数来打开文件,使用 Close()函数关闭文件,在实际写代码时,我们一般要使用err来判断是否出现错误以及获取错误信息
err是必须写的,因为Open()函数内部是这样定义的
func Open(name string) (*File, error)
下面是演示代码
package main
import (
"fmt"
"os"
)
func main() {
// 只读方式打开当前目录下的main.go文件
file, err := os.Open("src/Practice/test01.txt")
if err != nil {
fmt.Println("open file failed!, err:", err)
return
}
// 关闭文件
defer file.Close()
}
上面这段代码编译通过,但不会有任何结果,因为我们写的是正确的代码。
如果我们被读取的文件路径出现错误,即os.Open()函数括号里的内容不对,则通常会报这样的错误
defer file.Close()
而不是
file.Close()
在Go语言中,使用defer关键字是为了来延迟函数的执行直到包含它的函数即将返回。
而在本例中,使用defer,即使出现异常或提前返回,也能释放资源,减少重复代码,提高代码可读性和维护性,降低因遗漏关闭操作导致的错误。简单来说,就是这样写代码健壮性更好。
较为常见的是使用Read()方法,其定义如下
func (f *File) Read(b []byte) (n int, err error)
这方法接受一个字节切片,并返回读取的字节数和可能的具体错误
package main
import (
"fmt"
"io"
"os"
)
func main() {
//以只读方式打开当前目录下的test01.txt文件
file, err := os.Open("src/Practice/test01.txt")
if err != nil {
fmt.Println("file open failed, err:", err)
return
}
//defer关闭文件
defer file.Close()
//使用Read方法读取数据
//声明一个byte类型的切片,
、、初始化长度为128字节,用来存储读取到的数据
var tmp = make([]byte, 128)
//调用文件对象的Read方法,将读取到的数据存入tmp切片中,
//这里Read方法返回两个值,n表示实际读取到的字节数,err表示在读取过程中是否发生错误
n, err := file.Read(tmp)
//如果err值等于io.EOF,则表示文件已经读取到末尾,(EOF:End Of File,文件末尾)
if err == io.EOF {
fmt.Println("file read finished")
return
//检查err是否为nil,如果是,则打印错误信息并返回,结束执行
} else if err != nil {
fmt.Println("read file failed, err:", err)
return
}
/* 打印读取到的数据字节总长度,以及读取到的数据
因为本例中实际上被读取文件test01.txt有6968个字节,大于我们给tmp切片初始化的128字节,
故这里实际上只能读取到128个字节病并将其打印,可以通过修改初始化存储上限而读取到全部字节*/
fmt.Printf("读取了%d字节数据\n", n)
fmt.Println(string(tmp[:n]))
}
关于代码的解释我都写在注释里
上面这个例子中我们初始化一个切片来存放我们读取到的数据,但实际使用时我们会发现,很多时候我们初始化的切片长度不足以读取文件内的所有数据,这个时候我们就要想办法能够读取到文件内的所有数据,一个常见的办法就是循环读取
package main
import (
"fmt"
"io"
"os"
)
func main() {
//只读方式打开当前目录下的文件
file, err := os.Open("src/Practice/test01.txt")
if err != nil {
fmt.Println("open file failed, err: ", err)
return
}
//defer关闭文件
defer file.Close()
//for循环读取文件内容
//声明一个content变量,初始化为byte类型的切片,用于存储读取到的所有数据
var content []byte
//声明一个byte类型的切片,初始化长度为128字节,
//用于存储读取到的每一次读取到的数据,for循环为多次读取
var tmp = make([]byte, 128)
for {
n, err := file.Read(tmp)
if err == io.EOF {
fmt.Println("finish")
break
}
if err != nil {
fmt.Println("read file failed ,err:", err)
return
}
/*将每次读取到的数据追加到content切片中,
这里使用了append函数和扩展语法...来将tmp[:n]作为一个整体追加到content切片中。
*/
content = append(content, tmp[:n]...)
}
fmt.Println(string(content))
}
上面这个例子中我没有打印读取到的字节数,因为在第一个例子中已经打印过了,这两个例子读取的都是同一个文件。
接下来再看一个使用for循环的例子,但这个例子的主角是bufio,在Go语言中,bufio
是一个缓冲 I/O 的包,它在I/O操作的基础上增加了缓冲和一些帮助函数用于简化操作
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//只读方式打开当前目录下的文件
file, err := os.Open("src/Practice/test01.txt")
if err != nil {
fmt.Println("open file failed, err: ", err)
return
}
//defer关闭文件
defer file.Close()
/* 创建一个信的bufio.Scanner对象,用于从file中读取数据,
bufio.Scanner可以逐行读取文件内容*/
scanner := bufio.NewScanner(file)
//在循环内部,使用scanner.Text()方法获取当前行的文本,并使用fmt.Println打印出来。
for scanner.Scan() {
fmt.Println(scanner.Text())
}
//循环结束后,检查scanner在读取过程中是否遇到了错误。如果有错误,程序将打印错误信息。
if err := scanner.Err(); err != nil {
fmt.Println("read file failed, err:", err)
}
}
那么这两个方法有什么区别呢,简单来讲,使用 bufio.Reader
的 ReadString
方法更适合于逐行读取文件,因为它提供了缓冲和自动处理行结束符的便利。而直接使用 os.File
的 Read
方法则提供了更多的灵活性,但需要手动处理更多的细节。
那么有没有不用循环就可以读取整个文件的方法嘞
包有的,os
(Go1.16之前io/ioutil
)包的ReadFile
函数能够读取完整的文件,只需要将文件名作为参数传入。
package main
import (
"fmt"
"os"
)
// os.ReadFile 读取整个文件
func main() {
content, err := os.ReadFile("src/Practice/test01.tx")
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Println(string(content))
}
接下来我们来看看文件的写入操作
os.OpenFile()
函数能够以指定模式打开文件,从而实现文件写入相关功能。
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
...
}
其中:
name
:要打开的文件名 flag
:打开文件的模式。 模式有以下几种:
模式 | 含义 |
---|---|
os.O_WRONLY |
只写 |
os.O_CREATE |
创建文件 |
os.O_RDONLY |
只读 |
os.O_RDWR |
读写 |
os.O_TRUNC |
清空 |
os.O_APPEND |
追加 |
perm
:文件权限,一个八进制数。r(读)04,w(写)02,x(执行)01。
package main
import (
"fmt"
"os"
)
func main() {
//使用OpenFile函数,创建,清空,只写一个文件
file, err := os.OpenFile("src/Practice/test02.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("failed, err:", err)
return
}
defer file.Close()
str := "aijkodhoia\n"
//将str字符串转换为字节切片,并写入文件
var _, err02 = file.Write([]byte(str))
if err02 != nil {
fmt.Println("err:", err)
return
}
//直接将字符串写入文件
file.WriteString("4365468458")
}
在上面的代码中,我们注意到有一串数字0666,那么这是个啥呢
其实,0666是一个八进制数,表示文件所有者、所属组和其他用户都有读(4)和写(2)权限即:所有者(owner):读 + 写 = 4 + 2 = 6 所属组(group):读 + 写 = 4 + 2 = 6 其他用户(others):读 + 写 = 4 + 2 = 6 如果你需要设置不同的权限,可以修改这个八进制数。
例如: 0644:所有者有读和写权限,所属组和其他用户只有读权限。 0755:所有者有读和写权限,所属组和其他用户有读和执行权限。 0700:只有所有者有读和写权限,所属组和其他用户没有任何权限。
os
(Go1.16之前io/ioutil
)包的os.WriteFile
函数可直接向文件写入指定内容。
package main
import (
"fmt"
"os"
)
func writeFile() {
//声明一个变量,标明要写入的内容
str := "hello,沙河"
//os.WriteFile()方法可以直接写入字符串内容
err := os.WriteFile("src/Practice/test02.txt", []byte(str), 0666)
if err != nil {
fmt.Println("failed; err:", err)
return
}
}
func main() {
writeFile()
}
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//前面创建清空写入操作部分都一样,类比于文件读取操作中的打开和关闭文件都一样
file, err := os.OpenFile("src/Practice/test02.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
fmt.Println("failed, err:", err)
return
}
defer file.Close()
//bufio创建一个对象
writer := bufio.NewWriter(file)
//调用WriteString方法,for循环写入数据
for i := 1; i <= 10; i++ {
writer.WriteString("456123\n")
}
writer.Flush()
}
writer.Flush()
有同学可能注意到,使用bufio.Writer写入时我们在最后写了一个writer.Flush,那么这是个啥呢,其实writer.Flush()
方法是 bufio.Writer
对象的一个方法,用于将缓冲区中的数据强制写入底层的 io.Writer
对象中。bufio.Writer
通常用于提供缓冲的写入功能,这意味着数据首先被写入到内存中的缓冲区里,而不是直接写入文件或其他输出流。
使用writer.Flush()
方法有两个好处:
缓冲:bufio.Writer
会将写入的数据缓存起来,直到缓冲区满了或者显式调用 Flush()
方法,才会将数据写入到底层的文件或输出流中。这意味着如果你写入少量数据,它可能不会立即被写入磁盘。
数据完整性:在某些情况下,你需要确保所有数据都被写入到文件中,特别是在程序可能会在写入操作后很快退出时。调用 Flush()
可以确保所有缓冲中的数据都被写入,避免数据丢失。
以上就是笔者对于Go语言文件操作的所有理解,一定有些遗漏和不足之处,欢迎各位读者在评论区指正