《Go语言四十二章经》第三十一章 文件操作与IO

作者:李骁

31.1 文件系统

对于文件和目录的操作,Go主要在os 提供了的相应函数:

  1. func Mkdir(name string, perm FileMode) error
  2. func Chdir(dir string) error
  3. func TempDir() string
  4. func Rename(oldpath, newpath string) error
  5. func Chmod(name string, mode FileMode) error
  6. func Open(name string) (*File, error) {
  7. return OpenFile(name, O_RDONLY, 0)
  8. }
  9. func Create(name string) (*File, error) {
  10. return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
  11. }
  12. func OpenFile(name string, flag int, perm FileMode) (*File, error) {
  13. testlog.Open(name)
  14. return openFileNolog(name, flag, perm)
  15. }

从上面函数定义中我们可以发现一个情况:那就是os中不同函数打开(创建)文件的操作,最终还是通过OpenFile来实现,而OpenFile由编译器根据系统的情况来选择不同的底层功能来实现,对这个实现细节有兴趣可以根据标准包来仔细了解,这里就不展开讲了。

  1. os.Open(name string) 使用只读模式打开文件;
  2. os.Create(name string) 创建新文件,如文件存在则原文件内容会丢失;
  3. os.OpenFile(name string, flag int, perm FileMode) 这个函数可以指定flagFileMode 。这三个函数都会返回一个文件对象。
  1. Flag
  2. O_RDONLY int = syscall.O_RDONLY // 只读打开文件和os.Open()同义
  3. O_WRONLY int = syscall.O_WRONLY // 只写打开文件
  4. O_RDWR int = syscall.O_RDWR // 读写方式打开文件
  5. O_APPEND int = syscall.O_APPEND // 当写的时候使用追加模式到文件末尾
  6. O_CREATE int = syscall.O_CREAT // 如果文件不存在,此案创建
  7. O_EXCL int = syscall.O_EXCL // 和O_CREATE一起使用,只有当文件不存在时才创建
  8. O_SYNC int = syscall.O_SYNC // 以同步I/O方式打开文件,直接写入硬盘
  9. O_TRUNC int = syscall.O_TRUNC // 如果可以的话,当打开文件时先清空文件

在ioutil包中,也可以对文件操作,主要有下面三个函数:

  1. func ReadFile(filename string) ([]byte, error) // f, err := os.Open(filename)
  2. func WriteFile(filename string, data []byte, perm os.FileMode) error //os.OpenFile
  3. func ReadDir(dirname string) ([]os.FileInfo, error) // f, err := os.Open(dirname)

这三个函数涉及到了文件IO ,而对文件的操作我们除了打开(创建),关闭外,更主要的是对内容的读写操作上,也即是文件IO处理上。在Go语言中,对于IO的操作在Go 语言很多标准库中存在,很难完整地讲清楚。下面我就尝试结合io, ioutil, bufio这三个标准库,讲一讲这几个标准库在文件IO操作中的具体使用方法。

31.2 IO读写

Go 语言中,为了方便开发者使用,将 IO 操作封装在了大概如下几个包中:

  • io 为 IO 原语(I/O primitives)提供基本的接口
  • io/ioutil 封装一些实用的 I/O 函数
  • fmt 实现格式化 I/O,类似 C 语言中的 printf 和 scanf ,后面会详细讲解
  • bufio 实现带缓冲I/O

在 io 包中最重要的是两个接口:Reader 和 Writer 接口。

这两个接口是我们了解整个IO的关键,我们只要记住:实现了这两个接口,就有了 IO 的功能

有关缓冲:

  • 内核中的缓冲:无论进程是否提供缓冲,内核都是提供缓冲的,系统对磁盘的读写都会提供一个缓冲(内核高速缓冲),将数据写入到块缓冲进行排队,当块缓冲达到一定的量时,才把数据写入磁盘。

  • 进程中的缓冲:是指对输入输出流进行了改进,提供了一个流缓冲,当调用一个函数向磁盘写数据时,先把数据写入缓冲区,当达到某个条件,如流缓冲满了,或刷新流缓冲,这时候才会把数据一次送往内核提供的块缓冲中,再经块化重写入磁盘。

Go 语言提供了很多读写文件的方式,一般来说常用的有三种。
一:os.File 实现了Reader 和 Writer 接口,所以在文件对象上,我们可以直接读写文件。

  1. func (f *File) Read(b []byte) (n int, err error)
  2. func (f *File) Write(b []byte) (n int, err error)

在使用File.Read读文件时,可考虑使用buffer:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. )
  6. func main() {
  7. b := make([]byte, 1024)
  8. f, err := os.Open("./tt.txt")
  9. _, err = f.Read(b)
  10. f.Close()
  11. if err != nil {
  12. fmt.Println(err)
  13. }
  14. fmt.Println(string(b))
  15. }

二:ioutil库,没有直接实现Reader 和 Writer 接口,但是通过内部调用,也可读写文件内容:

  1. func ReadAll(r io.Reader) ([]byte, error)
  2. func ReadFile(filename string) ([]byte, error) //os.Open
  3. func WriteFile(filename string, data []byte, perm os.FileMode) error //os.OpenFile
  4. func ReadDir(dirname string) ([]os.FileInfo, error) // os.Open

三:使用bufio库,这个库实现了IO的缓冲操作,通过内嵌io.Reader、io.Writer接口,新建了Reader ,Writer 结构体。同时也实现了Reader 和 Writer 接口。

  1. type Reader struct {
  2. buf []byte
  3. rd io.Reader // reader provided by the client
  4. r, w int // buf read and write positions
  5. err error
  6. lastByte int
  7. lastRuneSize int
  8. }
  9. type Writer struct {
  10. err error
  11. buf []byte
  12. n int
  13. wr io.Writer
  14. }
  15. func (b *Reader) Read(p []byte) (n int, err error)
  16. func (b *Writer) Write(p []byte) (nn int, err error)

这三种读方式的效率怎么样呢,我们可以看看:

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "time"
  9. )
  10. func read1(path string) {
  11. fi, err := os.Open(path)
  12. if err != nil {
  13. panic(err)
  14. }
  15. defer fi.Close()
  16. buf := make([]byte, 1024)
  17. for {
  18. n, err := fi.Read(buf)
  19. if err != nil && err != io.EOF {
  20. panic(err)
  21. }
  22. if 0 == n {
  23. break
  24. }
  25. }
  26. }
  27. func read2(path string) {
  28. fi, err := os.Open(path)
  29. if err != nil {
  30. panic(err)
  31. }
  32. defer fi.Close()
  33. r := bufio.NewReader(fi)
  34. buf := make([]byte, 1024)
  35. for {
  36. n, err := r.Read(buf)
  37. if err != nil && err != io.EOF {
  38. panic(err)
  39. }
  40. if 0 == n {
  41. break
  42. }
  43. }
  44. }
  45. func read3(path string) {
  46. fi, err := os.Open(path)
  47. if err != nil {
  48. panic(err)
  49. }
  50. defer fi.Close()
  51. _, err = ioutil.ReadAll(fi)
  52. }
  53. func main() {
  54. file := "" //找一个大的文件,如日志文件
  55. start := time.Now()
  56. read1(file)
  57. t1 := time.Now()
  58. fmt.Printf("Cost time %v\n", t1.Sub(start))
  59. read2(file)
  60. t2 := time.Now()
  61. fmt.Printf("Cost time %v\n", t2.Sub(t1))
  62. read3(file)
  63. t3 := time.Now()
  64. fmt.Printf("Cost time %v\n", t3.Sub(t2))
  65. }

经过多次测试,基本上保持 bufio < ioutil < file.Read 这样的成绩, bufio读同一文件耗费时间最少, 效果稳稳地保持在最佳。

31.3 ioutil包

下面代码使用ioutil包实现2种读文件,1种写文件的方法,其中 ioutil.ReadAll 可以读取所有io.Reader 流。所以在网络连接中,也经常使用ioutil.ReadAll 来读取流,后面章节我们会讲到这块内容。

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "os"
  6. )
  7. func main() {
  8. fileObj, err := os.Open("./tt.txt")
  9. defer fileObj.Close()
  10. Contents, _ := ioutil.ReadAll(fileObj)
  11. fmt.Println(string(contents))
  12. if contents, _ := ioutil.ReadFile("./tt.txt"); err == nil {
  13. fmt.Println(string(contents))
  14. }
  15. ioutil.WriteFile("./t3.txt", contents, 0666)
  16. }

31.4 bufio包

bufio 包通过 bufio.NewReader 和bufio.NewWriter 来创建IO 方法集,利用缓冲来处理流,后面章节我们也会讲到这块内容。

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. )
  7. func main() {
  8. fileObj, _ := os.OpenFile("./tt.txt", os.O_RDWR|os.O_CREATE, 0666)
  9. defer fileObj.Close()
  10. Rd := bufio.NewReader(fileObj)
  11. cont, _ := Rd.ReadSlice('#')
  12. fmt.Println(string(cont))
  13. Wr := bufio.NewWriter(fileObj)
  14. Wr.WriteString("WriteString writes a ## string.")
  15. Wr.Flush()
  16. }
  1. 程序输出:
  2. WriteString writes a #

bufio包中,主要方法如下:

  1. // NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象,缓存大小由 size 指定(如果小于 16 则会被设置为 16)。
  2. func NewReaderSize(rd io.Reader, size int) *Reader
  3. // NewReader 相当于 NewReaderSize(rd, 4096)
  4. func NewReader(rd io.Reader) *Reader
  5. // Peek 返回缓存的一个切片,该切片引用缓存中前 n 个字节的数据。
  6. // 如果 n 大于缓存的总大小,则返回 当前缓存中能读到的字节的数据。
  7. func (b *Reader) Peek(n int) ([]byte, error)
  8. // Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误。
  9. // 如果缓存不为空,则只能读出缓存中的数据,不会从底层 io.Reader
  10. // 中提取数据,如果缓存为空,则:
  11. // 1、len(p) >= 缓存大小,则跳过缓存,直接从底层 io.Reader 中读出到 p 中。
  12. // 2、len(p) < 缓存大小,则先将数据从底层 io.Reader 中读取到缓存中,
  13. // 再从缓存读取到 p 中。
  14. func (b *Reader) Read(p []byte) (n int, err error)
  15. // Buffered 该方法返回从当前缓存中能被读到的字节数。
  16. func (b *Reader) Buffered() int
  17. // Discard 方法跳过后续的 n 个字节的数据,返回跳过的字节数。
  18. func (b *Reader) Discard(n int) (discarded int, err error)
  19. // ReadSlice 在 b 中查找 delim 并返回 delim 及其之前的所有数据。
  20. // 该操作会读出数据,返回的切片是已读出的数据的引用,切片中的数据在下一次
  21. // 读取操作之前是有效的。
  22. // 如果找到 delim,则返回查找结果,err 返回 nil。
  23. // 如果未找到 delim,则:
  24. // 1、缓存不满,则将缓存填满后再次查找。
  25. // 2、缓存是满的,则返回整个缓存,err 返回 ErrBufferFull。
  26. // 如果未找到 delim 且遇到错误(通常是 io.EOF),则返回缓存中的所有数据
  27. // 和遇到的错误。
  28. // 因为返回的数据有可能被下一次的读写操作修改,所以大多数操作应该使用
  29. // ReadBytes 或 ReadString,它们返回的是数据的拷贝。
  30. func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
  31. // ReadLine 是一个低水平的行读取原语,大多数情况下,应该使用ReadBytes('\n')
  32. // 或 ReadString('\n'),或者使用一个 Scanner。
  33. // ReadLine 通过调用 ReadSlice 方法实现,返回的也是缓存的切片。
  34. // 用于读取一行数据,不包括行尾标记(\n 或 \r\n)。
  35. // 只要能读出数据,err 就为 nil。如果没有数据可读,则 isPrefix
  36. // 返回 false,err 返回 io.EOF。
  37. // 如果找到行尾标记,则返回查找结果,isPrefix 返回 false。
  38. // 如果未找到行尾标记,则:
  39. // 1、缓存不满,则将缓存填满后再次查找。
  40. // 2、缓存是满的,则返回整个缓存,isPrefix 返回 true。
  41. // 整个数据尾部“有一个换行标记”和“没有换行标记”的读取结果是一样。
  42. // 如果 ReadLine 读取到换行标记,则调用 UnreadByte 撤销的是换行标记,
  43. // 而不是返回的数据。
  44. func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
  45. // ReadBytes 功能同 ReadSlice,只不过返回的是缓存的拷贝。
  46. func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
  47. // ReadString 功能同 ReadBytes,只不过返回的是字符串。
  48. func (b *Reader) ReadString(delim byte) (line string, err error)
  49. // Reset 将 b 的底层 Reader 重新指定为 r,同时丢弃缓存中的所有数据,
  50. // 复位所有标记和错误信息。 bufio.Reader。
  51. func (b *Reader) Reset(r io.Reader)

下面一段代码是,里面有用到peek,Discard 等方法,可以修改方法参数值,仔细体会:

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "strings"
  6. )
  7. func main() {
  8. sr := strings.NewReader("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
  9. buf := bufio.NewReaderSize(sr, 0) //默认16
  10. b := make([]byte, 10)
  11. fmt.Println("==", buf.Buffered()) // 0
  12. S, _ := buf.Peek(5)
  13. fmt.Printf("%d == %q\n", buf.Buffered(), s) //
  14. nn, er := buf.Discard(3)
  15. fmt.Println(nn, er)
  16. for n, err := 0, error(nil); err == nil; {
  17. fmt.Printf("Buffered:%d ==Size:%d== n:%d== b[:n] %q == err:%v\n", buf.Buffered(), buf.Size(), n, b[:n], err)
  18. n, err = buf.Read(b)
  19. fmt.Printf("Buffered:%d ==Size:%d== n:%d== b[:n] %q == err: %v == s: %s\n", buf.Buffered(), buf.Size(), n, b[:n], err, s)
  20. }
  21. fmt.Printf("%d == %q\n", buf.Buffered(), s)
  22. }

有关IO 的处理,这里主要讲了针对文件的处理。后面在网络IO读写处理中,我们将会接触到更多的方式和方法。