Files
notes/resource/go/待总结/010 Go 语言 IO 流.md
2026-03-01 01:43:46 +08:00

700 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 计算机 I/O 与文件系统基础
## 数据的本质
计算机中的所有数据本质上都是二进制流,即由 0 和 1 组成的序列。不同类型的文件(如图片、视频、文本等)只是这些二进制数据按照特定格式编码和解码的结果。例如:
- 图片文件(JPG):二进制数据按照 JPEG 格式编码,显示器将其解析为可视化的图像
- 视频文件(MP4):二进制数据按照 MP4 编码规范存储,包含视频帧、音频等信息
- 文本文件:二进制数据通过字符编码(如 UTF-8)转换为可读的文字
## I/O 的含义
I/O(输入/输出)是计算机与外部世界交换数据的基础:
- Input(输入):从外部获取数据到计算机系统
- Output(输出):将数据从计算机系统传输到外部
## 文件系统中的文件类型
主要分为两大类:
1. 文本文件:存储人类可读的字符序列
2. 二进制文件:存储原始的二进制数据(如图片、视频、可执行文件等)
## 文件操作的基本功能
现代编程语言通常通过封装底层操作系统的文件 API 来提供文件操作能力。核心功能包括:
1. 获取文件信息(FileInfo
- 文件大小
- 创建时间
- 修改时间
- 访问权限等
2. 文件读取(Read
- 读取文本内容
- 读取二进制数据
- 支持随机访问和顺序读取
3. 文件写入(Write
- 写入文本内容
- 写入二进制数据
- 支持追加和覆盖模式
这种组织方式更系统地展示了这些概念之间的关系,并提供了更详细的解释。你觉得这样的组织方式如何?是否还需要在某些方面做进一步的调整?
```go
package main
import (
"fmt"
"os"
)
// file
// fileInfo
/*
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits 权限
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
// 获取更加详细的文件信息, *syscall.Stat_t 反射来获取
Sys() any // underlying data source (can return nil)
*/
func main() {
// 获取某个文件的状态
fileinfo, err := os.Stat("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\test")
if err != nil {
return
}
fmt.Println(fileinfo.Name()) // demo01.go
fmt.Println(fileinfo.IsDir()) // false
fmt.Println(fileinfo.ModTime()) // 2023-02-23 20:25:43.1772351 +0800 CST
fmt.Println(fileinfo.Size()) // 1186 字节数
fmt.Println(fileinfo.Mode()) // -rw-rw-rw-
}
```
## 创建文件、目录
通过代码创建文件
路径:
- 相对路径
- 相对当前目录的路径
- ./ 当前目录
- ../ 上一级目录
- 绝对路径
- 从盘符开始的路径
> 创建目录
mkdir / 权限
mkdirAll 创建层级目录
remove 删除目录
removeAll 强制删除目录
```go
package main
import (
"fmt"
"os"
)
// 创建目录
// 项目开源框架,一运行,就会自动生成脚手架目录
func main() {
// 打开一个文件夹(1、存在我就打开 2、不存在,创建这个文件夹)
// func Mkdir(name string, perm FileMode) error
// ModePerm : 0777
err := os.Mkdir("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file1", os.ModePerm)
if err != nil {
// 存在就无法创建了 Cannot create a file when that file already exists.
fmt.Println(err)
}
fmt.Println("文件夹创建完毕")
// 创建层级文件夹
err2 := os.MkdirAll("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2\\aa\\bb\\cc\\dd", os.ModePerm)
if err2 != nil {
fmt.Println(err2)
}
fmt.Println("层级文件夹创建完毕")
// 删除 remove
// func Remove(name string) error
// 通过remove方法只能删除单个空文件夹:
// remove D:\Environment\GoWorks\src\xuego\lesson11\file2: The directory is not empty.
err3 := os.Remove("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2")
if err3 != nil {
fmt.Println(err3)
//return
}
fmt.Println("file delete success!!")
// 如果存在多层文件,removeAll,相对来说比较危险,删除这个目录下的所有东西, 强制删除
err4 := os.RemoveAll("D:\\MyGo\\src\\github.com\\Docker7530\\testproject\\file2")
if err4 != nil {
fmt.Println(err4)
return
}
fmt.Println("err4 delete success!!")
}
```
> 创建文件
os.create(),若存在就是的打开-就是返回的这个file对象. 如果不存在,创建在打开
```go
package main
import (
"fmt"
"os"
)
func main() {
// 创建文件 Create
// func Create(name string) (*File, error) {
// 返回的file对象就是我们的文件
file1, err := os.Create("a.go") // 相对路径
if err != nil {
fmt.Println(err)
}
fmt.Println(file1)
// 删除
os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\a.go")
}
```
## IO 读
1、与文件建立连接
```go
package main
import (
"fmt"
"os"
)
// IO读
func main() {
// 找到这个文件的对象 create 创建、 打开Open
// func Open(name string) (*File, error)
file1, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\狂神的Go世界.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println(file1)
// file 类-- 指定的对象
// 打开文件的时候,选定权限: 可读可写的方式打开
// OpenFile(文件名,打开方式:可读、可写...,FileMode , 权限)
file2, err2 := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\狂神的Go世界.txt",
os.O_RDONLY|os.O_WRONLY, os.ModePerm)
if err2 != nil {
fmt.Println(err2)
}
fmt.Println(file2)
// 可以操作这个对象了
}
```
2、读取 file.Read([]byte) ,将file中的数据读取到 []byte 中, n,err n读取到的行数,err 错误,EOF错误,就代表文件读取完毕了
一直调用read,就代表光标往后移动….
```go
package main
import (
"fmt"
"os"
)
// 读取文件数据
func main() {
// 我们习惯于在建立连接时候通过defer来关闭连接,保证程序不会出任何问题,或者忘记关闭
// 建立连接
file, _ := os.Open("狂神的Go世界.txt")
// 关闭连接
defer file.Close()
// 读代码 ,Go 的错误机制,让我们专心可以写业务代码。
// 1、创建一个容器 (二进制文本文件--0100101010 => 读取流到一个容器 => 读取容器的数据)
bs := make([]byte, 2, 1024) // 缓冲区,可以接受我们读取的数据
// 2、读取到缓冲区中。 // 汉字一个汉字占 3个位置
n, err := file.Read(bs)
fmt.Println(n)
fmt.Println(err)
fmt.Println(string(bs)) // 读取到的字符串 ab
// 光标不停的向下去指向,读取出来的内容就存到我们的容器中。
file.Read(bs)
fmt.Println(string(bs)) // 读取到的字符串 cd
file.Read(bs)
fmt.Println(string(bs)) // 读取到的字符串 e
n, err = file.Read(bs)
fmt.Println(n)
fmt.Println(err) // EOF ,读取到了文件末尾。就会返回EOF。
fmt.Println(string(bs)) // 读取到的字符串
n, err = file.Read(bs)
fmt.Println(n)
fmt.Println(err)
fmt.Println(string(bs)) // 读取到的字符串
}
```
## IO 写(权限)
建立连接 (设置权限:可读可写,扩充这个文件的append os.OpenFile
关闭连接
写入 file.write
- file.Write
- file.WriteString
```go
package main
import (
"fmt"
"os"
)
func main() {
fileName := "狂神的Go世界.txt"
// 权限:如果我们要向一个文件中追加内容, O_APPEND, 如果没有,就是从头开始写
file, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
defer file.Close()
// 操作
bs := []byte{65, 66, 67, 68, 69} // A B C D E
n, err := file.Write(bs)
if err != nil {
fmt.Println(err)
}
fmt.Println(n)
// string类型的写入
n, err = file.WriteString("hhahahahah哈哈哈哈哈哈哈")
if err != nil {
fmt.Println(err)
}
fmt.Println(n)
}
```
## 文件复制
```go
package utils
import (
"fmt"
"io"
"os"
)
// Copy 方法需要参数为:source 源文件 ,destination 目标文件
func Copy(source, destination string, bufferSize int) {
// 读取文件
sourceFile, err := os.Open(source)
if err != nil {
fmt.Println("Open错误:", err)
}
// 输出文件 O_WRONLY , O_CREATE 如果不不存在,则会创建
destinationFile, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer sourceFile.Close()
defer destinationFile.Close()
// 专注业务代码,拷贝
buf := make([]byte, bufferSize)
// 读取
for {
n, err := sourceFile.Read(buf)
if n == 0 || err == io.EOF {
fmt.Println("读取完毕源文件,复制完毕")
break
} else if err != nil {
fmt.Println("读取错误:", err)
return // 错误之后,必须要return终止函数执行。
}
// 将缓冲区的东西写出到目标文件
_, err = destinationFile.Write(buf[:n])
if err != nil {
fmt.Println("写出错误:", err)
}
}
}
```
调用
```go
package main
import "xuego/lesson11/utils"
func main() {
source := "C:\\Users\\遇见狂神说\\Desktop\\xq.png"
dest := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\xq.png"
utils.Copy(source, dest, 1024)
}
```
> 系统给我们提供了copy方法
```go
// 调用系统的方法
func Copy2(source, destination string) {
// 读取文件
sourceFile, err := os.Open(source)
if err != nil {
fmt.Println("Open错误:", err)
}
// 输出文件 O_WRONLY , O_CREATE 如果不不存在,则会创建
destinationFile, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer sourceFile.Close()
defer destinationFile.Close()
// 具体的实现
written, err := io.Copy(destinationFile, sourceFile)
fmt.Println("文件的字节大小:", written)
}
```
## Seeker接口
设置光标的位置,读写文件
```go
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 1. 创建一个测试文件
file, err := os.OpenFile("狂神的Go世界1.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer file.Close()
// 2. 写入初始内容
file.WriteString("Hello World!")
// 3. 演示不同的 Seek 操作
demonstrateSeek(file)
}
func demonstrateSeek(file *os.File) {
// 重置光标到文件开始
file.Seek(0, io.SeekStart)
// 从文件开始读取 5 个字节
buf := make([]byte, 5)
n, err := file.Read(buf)
if err != nil {
fmt.Println("读取错误:", err)
return
}
fmt.Printf("1. 读取前5个字符: %s\n", string(buf[:n]))
// 从当前位置向后移动 1 个字节
file.Seek(1, io.SeekCurrent)
buf = make([]byte, 5)
n, _ = file.Read(buf)
fmt.Printf("2. 跳过1个字符后读取: %s\n", string(buf[:n]))
// 从文件末尾向前移动 5 个字节
file.Seek(-5, io.SeekEnd)
buf = make([]byte, 5)
n, _ = file.Read(buf)
fmt.Printf("3. 从末尾往前5个字符读取: %s\n", string(buf[:n]))
// 在文件末尾追加内容
file.Seek(0, io.SeekEnd)
file.WriteString("\nAppended Text")
// 读取整个文件内容
file.Seek(0, io.SeekStart)
content, _ := io.ReadAll(file)
fmt.Printf("\n4. 最终文件内容:\n%s\n", string(content))
}
```
## 断点续传
思考几个问题:
1、如果你要传的文件很大,70G,是否有方法可以缩短耗时?
- 将文件拆分
- 同时多线程进行下载
2、如果在文件传递过程中,程序被迫中断(断电、断网、内存满了..),下次重启之后,文件是否还需要重头再传?
- 希望能够继续上传或者下载
3、传递文件的时候,支持暂停和恢复上传?假设这个两个操作分布在重启前后?
- 支持!
file、read、write、seek
思路:
1、需要记住上一次传递了多少数据、temp.txt => 记录
2、如果被暂停或者中断了,我们就可以读取这个temp.txt的记录,恢复上传
3、删除temp.txt
![image-20230226203841579](../../../attachment/images-paste/image-20230226203841579.png)
**所有人必须要要全部理解的一段代码**
```go
package main
import (
"fmt"
"io"
"os"
"strconv"
)
// 断点续传
func main() {
// 传输源文件地址
srcFile := "C:\\Users\\遇见狂神说\\Desktop\\client\\gp.png"
// 传输的目标位置
destFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\server\\gp-upload.png"
// 临时记录文件
tempFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\temp.txt"
// 创建对应的file对象,连接起来
file1, _ := os.Open(srcFile)
file2, _ := os.OpenFile(destFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
file3, _ := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
defer file1.Close()
defer file2.Close()
fmt.Println("file1/2/3 文件连接建立完毕")
// 1、读取temp.txt
file3.Seek(0, io.SeekStart)
buf := make([]byte, 1024, 1024)
n, _ := file3.Read(buf)
// 2、转换成string - 数字。
countStr := string(buf[:n])
count, _ := strconv.ParseInt(countStr, 10, 64)
fmt.Println("temp.txt中记录的值为:", count) // 5120
// 3、设置读写的偏移量
file1.Seek(count, io.SeekStart)
file2.Seek(count, io.SeekStart)
fmt.Println("file1/2 光标已经移动到了目标位置")
// 4、开始读写(复制、上传)
bufData := make([]byte, 1024, 1024)
// 5、需要记录读取了多少个字节
total := int(count)
for {
// 读取数据
readNum, err := file1.Read(bufData)
if err == io.EOF { // file1 读取完毕了
fmt.Println("文件传输完毕了")
file3.Close()
os.Remove(tempFile)
break
}
// 向目标文件中写入数据
writeNum, err := file2.Write(bufData[:readNum])
// 将写入数据放到 total中, 在这里total 就是传输的进度
total = total + writeNum
// temp.txt 存放临时记录数据
file3.Seek(0, io.SeekStart) // 将光标重置到开头
file3.WriteString(strconv.Itoa(total))
}
}
```
## 遍历文件夹
**下去一定要自己实现**
```go
package main
import (
"fmt"
"log"
"os"
)
// cd /d 文件夹路径
// tree /F , 查看当前文件夹下的所有文件
// 遍历文件夹
// 1、读取当前文件夹下的所有文件
// 2、如果是文件夹,进入文件夹,继续读取里面的所有文件
// 3、设置一些结构化代码
func main() {
dir := "D:\\Environment\\GoWorks\\src\\xuego"
tree(dir, 0)
}
// 日常调试测试常用fmt输出 、 工作中or项目中更多是log日志输出
func tree(dir string, level int) {
// 编写层级
tabString := "|--"
for i := 0; i < level; i++ {
tabString = "| " + tabString
}
// 获取目录 ReadDir, 返回目录信息[]DirEntry,多个文件信息
fileInfos, err := os.ReadDir(dir)
if err != nil {
log.Println(err)
}
// 遍历出来所有文件之后,获取里面的单个文件
for _, file := range fileInfos {
// 文件夹中文件的全路径展示
filename := dir + "\\" + file.Name()
fmt.Println(tabString + file.Name())
// 如果是文件夹,再次遍历
if file.IsDir() {
tree(filename, level+1)
}
}
}
```
## bufio
Go语言自带的IO操作包。bufio,使用这个包可以大幅提升文件的读写效率。
buf 缓冲区.
io操作效率本身是还可以的,频繁访问本地磁盘文件(效率低)
所以说 bufio ,提供了一个缓冲区,读和写都先在缓冲区中,最后再一次性读取或者写入到文件里,降低访问本地磁盘的次数。
![image-20230226214440096](../../../attachment/images-paste/image-20230226214440096.png)
bufio写入
```go
package main
import (
"bufio"
"fmt"
"log"
"os"
)
// bufio 的应用
func main() {
file, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\demo01.go")
if err != nil {
log.Println(err)
}
defer file.Close()
// 读取文件
// 创建一个bufio包下的 reader对象。
//bufioReader := bufio.NewReader(file)
//buf := make([]byte, 1024)
//n, err := bufioReader.Read(buf)
//fmt.Println("读取到了多少个字节:", n)
// 读取键盘的输入
// 键盘的输入,实际上是流 os.Stdin
inputReader := bufio.NewReader(os.Stdin)
// delim 到哪里结束读取
readString, _ := inputReader.ReadString('\n')
fmt.Println("读取键盘输入的信息:", readString)
}
```
bufio写出
```go
package main
import (
"bufio"
"fmt"
"os"
)
// 写入
func main() {
file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson11\\a.txt",
os.O_RDWR|os.O_CREATE,
os.ModePerm)
defer file.Close()
// bufio
fileWrite := bufio.NewWriter(file)
writeNum, _ := fileWrite.WriteString("kuangshen")
fmt.Println("writeNum:", writeNum)
// 发现并没有写出到文件,是留在了缓冲区,所以我们需要调用 flush 刷新缓冲区
// 手动刷新进文件
fileWrite.Flush()
}
```