1. 文件读写操作
在我们对一个文件进行读写操作前,有一个必做步骤,那就是要先打开文件。
打开文件主要使用os模块的 Open 和 OpenFile 。
- Open:适合读。
- OpenFile:适合读写。
2. 打开文件
2.1 Open
作用:
以只读方式打开文件,权限使用文件默认权限。
下面是Open的源码:
func Open(name string) (*File, error) { // O_RDONLY:只读方式打开文件,文件必须存在。 // 0:在 os.OpenFile 函数中,传入0作为权限参数意味着使用默认的文件权限。 return OpenFile(name, O_RDONLY, 0) }
语法:
func os.Open(name string) (*os.File, error)
参数含义:
name string:文件路径变量。
*os.File:表示打开的文件对应的指针。
error:可能发生的错误,只要有异常,error类型就会被赋值。
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abc
f, err := os.Open(filename)
if err != nil {
panic(err)
}
// 用完后一定要关闭文件,释放文件描述符
defer f.Close() // 可结合defer,让文件关闭操作,最后一定会执行
fmt.Printf("f的类型=%T\nf的值=%[1]v\nf的值=%#[1]v", f)}
==========调试结果==========
f的类型=*os.File
f的值=&{0xc000004a00}
f的值=&os.File{file:(*os.file)(0xc000004a00)}
上述代码打开文件后得到一个文件操作句柄(&{0xc000004a00}),这是os.File类型的指针,使用它就可以操作文件了。
2.2 Read
作用:
Read 方法是 os.File 结构体的一个方法,用于从文件中读取数据。
Read是按字节读取的文件,在文件结束时,Read返回0,io.EOF。
语法:
func (r *File) Read(b []byte) (n int, err error)
参数:
b []byte:一个字节切片,用来存储读取的数据。
n int:实际读取到的字节数。
err error:如果有错误发生,返回错误信息。
注意事项:使用Read方法的前提,一定是已经用Open方法打开文件,并获得了文件指针。
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
f, err := os.Open(filename)
if err != nil {
panic(err)
}
// 用完后一定要关闭文件,释放文件描述符
defer fmt.Println("文件关闭完成")
defer f.Close() // 可结合defer,确保文件最后一定会关闭。
defer fmt.Println("开始关闭文件")
fmt.Println("返回的文件指针(文件句柄):", f)
// 读取文件
// 因为Read方法,是需要一个字节切片来作为参数的,所以必须要先定义一个字节切片。
// 正常读取,都是k的整数倍,如1024、2048等。这里写3,只是为了测试
buffer := make([]byte, 3) // 创建一个大小为 2 字节的缓冲区(字节切片)
n, err := f.Read(buffer) // 从文件中读取数据到缓冲区,n表示成功读取了n个字节
if err != nil {
panic(err)
} else {
fmt.Printf("成功读取的字节数:%d\n读取缓冲区:%d\n字节转换字符串:%s\n", n, buffer, string(buffer))
}
}
==========调试结果==========
返回的文件指针(文件句柄): &{0xc000148780}
成功读取的字节数:3
读取缓冲区:[97 98 99]
字节转换字符串:abc
开始关闭文件
文件关闭完成
2.2.1 循环读取文件
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
fmt.Println("返回的文件指针(文件句柄):", f)
// 读取文件
buffer := make([]byte, 1)
// 无限循环读取,每次读取1个字节
for {
n, err := f.Read(buffer)
if err != nil {
// panic(err)
fmt.Println(err)
} else {
fmt.Printf("成功读取的字节数:%d\n读取缓冲区:%d\n字节转换字符串:%s\n", n, buffer, string(buffer))
fmt.Println("==================")
}
}
}
通过断点执行发现,文件内容读取完毕后,会返回一个EOF,表示已无内容可读取,字节数也为0。
可以在判断中加上painc或break,捕获到异常就停止运行。
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
fmt.Println("返回的文件指针(文件句柄):", f)
// 读取文件
buffer := make([]byte, 1)
for {
n, err := f.Read(buffer)
if err != nil {
fmt.Println(err)
break
// panic(err)
} else {
fmt.Printf("成功读取的字节数:%d\n读取缓冲区:%d\n字节转换字符串:%s\n", n, buffer, string(buffer))
fmt.Println("==================")
}
}
}
==========调试结果==========
返回的文件指针(文件句柄): &{0xc0000cc780}
成功读取的字节数:1
读取缓冲区:[97]
字节转换字符串:a
==================
成功读取的字节数:1
读取缓冲区:[98]
字节转换字符串:b
==================
成功读取的字节数:1
读取缓冲区:[99]
字节转换字符串:c
==================
EOF
开始关闭文件
文件关闭完成
2.2.2 buffer越界
上述代码还有一个问题,如果读取的字节数大于buffer中剩余可读的字节数,会出现下面这种问题:
这个buffer越界是这样的,如上述代码,每次读取2字节,第一次读取ab放到buferr中,第二次读取到c和没有存储数据的空字节,但是由于第二次读取的结果,是覆盖上一次的结果的,所以我们看到的是cb。
解决办法:buffer[:n],从0开始到n-1,就能避免越界。
2.2.3 文件读取总结
- 读取完毕后,一定要关闭文件释放文件描述符,如:f.Close()或defer f.Close()。
- 读取文件异常时,一定要panic或break。
- 读取文件时,正确的做法应该是buffer[:n],防止buffer越界,特别是string(buffer[:n])。
- 建议每次读取大小为KB的整数倍,特别是大文件,如1024字节、2048字节等。
3. 定位(Seek)
文件是什么?是二进制有序序列,逻辑上理解为字节数组。
也就是说,文件读写都会有一个针,指向二进制序列的索引,正常都是从前向后移动。那么如何定位到某一个指定的字节呢?这里就要介绍下Seek(定位or寻找)。
3.1 Seek函数介绍
作用:
在Go语言中,
Seek
函数是用来设置文件读写操作中的偏移量的。Seek
函数定义在os
包的File
结构体中,其原型如下:语法:
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
参数:
offset:相对偏移量。
whence:决定这个偏移量是相对于文件的哪个位置。并且whence还有3个选项:
0
表示相对文件开头 (io.SeekStart
),offset 只能正,负报错。- 1 表示相对当前位置 (
io.SeekCurrent
),offset 可正可负,但是负指针不能超左边界。- 2 表示相对文件结尾 (
io.SeekEnd
),offset 可正可负,但是负指针不能超左边界,正数的话,会读不到内容,但不会报错。常用:
- Seek(0, 0),指针拉到文件开头读写。
- Seek(0, 2),指针拉到文件结尾读写。
3.2 示例
3.2.1 二次读取文件
注意这里调整了文件内容为:abcdef
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abcdef
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
fmt.Println("返回的文件指针(文件句柄):", f)
// 读取文件
buffer := make([]byte, 2)// len=2, cap=2
for {
n, err := f.Read(buffer)
if err != nil {
fmt.Println(err)
break
// panic(err)
} else {
fmt.Printf("成功读取的字节数:%d\n读取缓冲区:%d\n字节转换字符串:%s\n", n, buffer[:n], string(buffer[:n]))
fmt.Println("==================")
}
}
fmt.Println("==================") // 新增内容
n, err := f.Read(buffer)
fmt.Println(n, err)
}
==========调试结果==========
省略部分输出
==================
0 EOF
开始关闭文件
文件关闭完成
从上面的输出结果可以看到,当我们二次读取一个已经被读取完毕的文件时,只能读取到0字节和EOF,这是因为此时的指针已经指向了文件结尾,已经没有字节可以读取了。
如果想继续读取有效内容,可以调整指针,也就是调整偏移量。
3.2.1.1 调整offset为负数,二次读取文件
错误的方式
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
fmt.Println("返回的文件指针(文件句柄):", f)
// 读取文件
buffer := make([]byte, 2)
for {
n, err := f.Read(buffer)
if err != nil {
fmt.Println(err)
break
// panic(err)
} else {
fmt.Printf("成功读取的字节数:%d\n读取缓冲区:%d\n字节转换字符串:%s\n", n, buffer[:n], string(buffer[:n]))
fmt.Println("==================")
}
}
fmt.Println("=======Seek=======")
ret, err := f.Seek(-2, 0) // 基于文件起始位置,往左移动2位。错误的方式
fmt.Println(ret, err)
}
==========调试结果==========
省略部分输出
=======Seek=======
0 seek D:/个人/学习/Go/文件与目录操作/test.txt: An attempt was made to move the file pointer before the beginning of the file.
开始关闭文件
文件关闭完成
可以看到,上述代码执行报错了,为什么?
首先for循环中,每次读取2个字节(buffer切片,长度容量都是指定为2了),后续每次读取的内容,都会覆盖上一次的内容,所以之前最终读取出来了cb,此时的指针是指在c这个位置的。
那么f.Seek(-2, 0),是说基于c这个位置(相当于就是文件内容起始位置),往左再偏移2位,从文件内容开头往左偏移,那肯定不行啊。
3.2.1.2 调整offset为正数,二次读取文件
这里把offset调整为了20,依然能够给正常执行,说明正向超界不会报错,虽然buffer切片的长度和容量都为2。
那这样做有啥意义呢?请继续往下看。
从上述结果来看,从文件末尾,正向读取的话,不会超界,最多没有内容。
3.2.1.3 从文件末尾往前读
3.3 Seek使用注意事项
在文件读写的过程中,不要随意使用Seek,建议文件内容EOF后,再使用Seek调整指针。
3.4 ReadAt
之前如果读取文件时,想调整偏移量,必须结合Seek和Read才行。
而ReadAt允许指定一个偏移量,然后从这个位置开始读取一定数量的字节到一个给定的缓冲区。
ReadAt相当于是从文件开头,开始偏移,但它不会影响当前文件指针。
但是Read和Write都会影响文件指针。
4. 自带缓冲读取(bufio)
文件使用Read读取,非常底层,操作起来很不方便,每次还要先创建一个buffer,来存储从磁盘中读取出来的字节序列,且bufio会自带一个指针,所以使用seek调整指针位置,是没用的。
Go语言提供了bufio包实现了对文件的二进制或文本处理的方法。
bufio 是 Go 语言标准库中用于缓冲输入输出的包。它提供了数据读写的缓冲机制,可以减少系统调用的次数,从而提高程序的效率。
4.1 bufio.NewReader
bufio.NewReader
是 Go 语言标准库中bufio
包的一个函数,它接收一个io.Reader
接口类型的参数,并返回一个*bufio.Reader
类型的值。这个返回的*bufio.Reader
包含了一个缓冲区,可以减少对底层输入流的直接调用次数。这里的
io.Reader
接口类型的参数,就是我们上边的f.Read(buffer),任何类型只要实现了Read(p []byte)
方法,就可以说它实现了io.Reader
接口。
4.1.1 Read
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abcdef
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
fmt.Println("=========NewReader=========")
reader := bufio.NewReader(f) // 这样就把f包装成了bufio的Reader
b1 := make([]byte, 3) // 新建用于存储文件内容的字节切片
n, err := reader.Read(b1)
fmt.Println(n, err, b1, string(b1[:n]))
}
=========NewReader=========
3 <nil> [97 98 99] abc
开始关闭文件
文件关闭完成
4.1.2 ReadByte(按单字节读取)
ReadByte读取并返回单个字节。如果没有可用的字节,则返回错误。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abcdef
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
reader := bufio.NewReader(f) // 这样就把f包装成了bufio的Reader
fmt.Println("=========ReadByte=========")
b, err2 := reader.ReadByte()
fmt.Println(b, err2, string(b))
}
=========ReadByte=========
97 <nil> a
开始关闭文件
文件关闭完成
4.1.3 ReadBytes(指定分隔符读取)
func (*bufio.Reader).ReadBytes(delim byte) ([]byte, error)
ReadBytes
函数读取输入直到遇到delim(
分隔符)
,返回一个包含数据直到(包括)分隔符的切片。如果
ReadBytes
在找到分隔符之前遇到错误,它将返回读取的数据和错误本身(通常是io.EOF
)。如果返回的数据不以
delim(
分隔符)
结尾,ReadBytes
将返回err != nil
。对于简单用途,使用Scanner
可能更方便。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abcdef
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
reader := bufio.NewReader(f) // 这样就把f包装成了bufio的Reader
fmt.Println("=========ReadBytes=========")
b, err2 := reader.ReadBytes('d') // 我文件中的内容:abcdef测试,这里我用d作为分隔符
fmt.Printf("返回的字节切片=%v\n切片转字符串=%s\nerr=%v\n", b, string(b), err2)
}
=========ReadBytes=========
返回的字节切片=[97 98 99 100]
切片转字符串=abcd
err=<nil>
开始关闭文件
文件关闭完成
4.1.4 ReadRune(读取rune字符)
作用:
从输入流中读取单个 UTF-8 编码的 Unicode 字符。
注意:使用rune读取,一定要保证文件的编码是UTF-8。
语法:
func (*bufio.Reader).ReadRune() (r rune, size int, err error)
- r rune:返回一个rune类型的字符。
- size int:返回值的字节大小。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt" // 文件内容:abcdef
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
reader := bufio.NewReader(f) // 这样就把f包装成了bufio的Reader
fmt.Println("=========ReadRune=========")
r, size, err2 := reader.ReadRune()
fmt.Printf("rune字符=%v\nrune字符大小=%d\nrune转str=%s\nerr=%v\n", r, size, string(r), err2)
}
=========ReadRune=========
rune字符=97
rune字符大小=1
rune转str=a
err=<nil>
开始关闭文件
文件关闭完成
4.1.5 ReadSlice(指定分隔符读取)
作用:
简单来说,就是指定一个byte分隔符,然后ReadSlice读取到第一个分隔符为止,返回读取到的切片,包含了分隔符和它之前的数据。
语法:
func (*bufio.Reader).ReadSlice(delim byte) (line []byte, err error)
- 参数 delim byte:分隔符
- 返回值 line []byte:读取到的数据切片
- 返回值 err error:错误
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
reader := bufio.NewReader(f)
fmt.Println("=========ReadSlice=========")
line, err := reader.ReadSlice('\n') // 文件内容是:abcdef\n测试
fmt.Println(line, string(line), err)
}
=========ReadSlice=========
[97 98 99 100 101 102 13 10] abcdef
<nil>
开始关闭文件
文件关闭完成
上述代码指定了分隔符为'\n',然后把截止到\n(包含\n)的所有数据都读出来了。
4.1.6 ReadString(指定分隔符读取,返回string)
作用:
ReadString读取,直到输入中第一次出现分隔符,返回一个字符串,其中包含分隔符之前的数据并包括分隔符。
如果ReadString在找到分隔符之前遇到错误,它将返回在错误之前读取的数据和错误本身(通常是io.EOF)。
ReadString返回err != nil当且仅当返回的数据不以delim结尾时。
语法:
func (*bufio.Reader).ReadString(delim byte) (string, error)
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 定义文件路径
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
// 打开文件
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// 读取文件
reader := bufio.NewReader(f)
fmt.Println("=========ReadString=========")
s, err := reader.ReadString('\n') // 文件内容是:abcdef\n测试
fmt.Println(s, err)
}
=========ReadString=========
abcdef
<nil> // 注意这里是包含了\n的
开始关闭文件
文件关闭完成
5. flag
5.1 基本介绍
flag 是一个非常有用的包,它允许你定义命令行参数,这样用户可以通过命令行来控制程序的行为。
flag包提供了一组函数和类型,让你可以定义命令行参数,比如开关选项、字符串、整数等。这些参数可以在程序启动时通过命令行指定。
5.2 可使用的模式
O_RDONLY:只读打开文件,用的较少,因为Open方法用的就是它。
O_WRONLY:只写
O_RDWR:读写
注意上面这三个,是互斥的,不可以同时使用。
O_APPEND:追加写入
O_CREATE:文件不存在则创建,存在就不管。
O_EXCL:文件存在则报错。配合O_CREATE使用,如果创建的文件已存在就报错。
O_SYNC:同步IO,等待上一次IO完成。
O_TRUNC:对于可写的文件,打开时清空内容。
6. 常用文件操作
这里结合上面的flag,就可以进行文件读写操作了。
// 只写打开文件,文件必须存在。
flag := os.O_WRONLY
// 这里有2层含义:
// (1)只写权限打开文件,指针在文件头部,写入的内容会覆盖后面存在的内容。
// (2)如果文件不存在,先创建,如果文件存在,不作操作。
flag = os.O_WRONLY | os.O_CREATE
// 2层含义:
// (1)只写权限打开文件,指针在文件头部,写入的内容会覆盖后面存在的内容。
// (2)如果文件不存在,先创建。如果文件存在,先把原有内容清空,再从头写入内容。
flag = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
// 这里有2层含义:
// (1)只写权限打开文件,文件不存在就先创建,指针在文件头部。
// (2)APPEND调整文件指针到文件末尾,新写入的内容在文件尾部追加进去。
flag = os.O_WRONLY | os.O_APPEND | os.O_CREATE
// 文件存在就报错
// 注意:不能单独使用,必须配合os.O_CREATE
flag = os.O_EXCL
// 只写权限打开文件,如果文件存在,则报错。文件不存在就创建文件,并从头开始写入。
flag = os.O_WRONLY | os.O_EXCL | os.O_CREATE
// 文件可读写(从文件头部开始),但要求文件存在
flag = os.O_RDWR
6.1 写入文件
6.1.1 WriteString
package main
import (
"fmt"
"os"
)
func main() {
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
// 读写方式打开文件
f, err := os.OpenFile(filename, os.O_RDWR, 0) // 0表示使用系统默认权限
if err != nil {
panic(err)
}
defer fmt.Println("文件关闭完成")
defer f.Close()
defer fmt.Println("开始关闭文件")
// flag操作
// 读取文件
buffer := make([]byte, 3)
n2, err2 := f.Read(buffer)
fmt.Println("读取文件:", n2, string(buffer[:n2]), err2)
// 写入文件
f.WriteString("rst") // 写入缓冲区
f.Sync() // 刷新缓冲中的内容到磁盘
}
==========调试结果==========
读取文件: 3 abc <nil>
开始关闭文件
文件关闭完成
上述代码,使用writestring写入了一个rst,如下图:
这里可以看到,WriteString写入的rst居然在abc的后面,为什么?
由于没有指定写入的位置,WriteString
方法将会把字符串写入到文件的当前读取/写入位置。
并且如果在读取操作之后没有移动文件的读写指针,那么写入的内容将会覆盖掉原来的位置,而不是追加到文件的末尾。
使用WriteString写入前,一定要注意指针的位置。
默认读、写打开,指针位置一定是在文件的开头,也就是索引0的位置。
6.1.2 flag使用总结
- O_WRONLY:只读,从头读,文件要存在
- O_WRONLY:只写,从头写,文件要存在。如果文件已存在有内容,从头覆盖
- O_CREATE | O_TRUNC:没有文件创建新文件,从头写;有文件清空内容从头写
- O_APPEND:追加写,文件要存在
- O_CREATE:文件存在,从头写;文件不存在创建新文件,从头写(单纯的create不含指针操作)
- O_EXCL | O_CREATE:文件不存在创建新文件,从头写;文件存在报错
- O_RDWR:既能读又能写,从头开始
6.2 带缓冲读写
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 文件内容:abcdef测试
filename := "D:/个人/学习/Go/文件与目录操作/test.txt"
// 提前配置flag
// 读写方式打开文件,如文件不存在,就创建,如果存在,就先清空内容。指针在文件头部。
flag := os.O_RDWR | os.O_CREATE | os.O_TRUNC
// os.ModePerm默认权限=0777,只影响新建文件
f, err := os.OpenFile(filename, flag, os.ModePerm)
//上面的openfile,还可以用create替代,create内部的flag设置和我们的flag配置一摸一样。
//f, err := os.Create(filename)
if err == nil {
defer f.Close()
// 定义读写打开文件
r := bufio.NewReader(f)
w := bufio.NewWriter(f)
// 写入内容
w.WriteString("0123456789\n")
w.WriteString("abc\n")
// 刷新缓冲区中的内容到磁盘
w.Flush()
fmt.Println("==========分割线==========")
// 经过上面的写入操作,此时的指针已经到了文件末尾,所以需要调整指针到文件开头
f.Seek(0, 0)
// 读取刚刚写入的内容,用换行符做分隔符。
fmt.Println(r.ReadString('\n'))
fmt.Println(r.ReadString('\n'))
} else {
fmt.Println("文件打开异常:", err)
}
}
==========分割线==========
0123456789
<nil>
abc
<nil>
6.3 文件写入总结
更加推荐bufio这种自带缓冲的方式,更加简单。
注意:追加和清空这俩flag,会改变文件指针位置,实际使用时需要注意。