# 计算机 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() } ```