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

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

作者: ffhelicopter | 来源:发表于2018-09-15 18:03 被阅读148次

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

    作者:李骁

    31.1 文件系统

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

    func Mkdir(name string, perm FileMode) error 
    func Chdir(dir string) error
    func TempDir() string
    func Rename(oldpath, newpath string) error
    func Chmod(name string, mode FileMode) error
    func Open(name string) (*File, error) {
        return OpenFile(name, O_RDONLY, 0)
    }
    func Create(name string) (*File, error) {
        return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
    }
    func OpenFile(name string, flag int, perm FileMode) (*File, error) {
        testlog.Open(name)
        return openFileNolog(name, flag, perm)
    }
    

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

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

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

    func ReadFile(filename string) ([]byte, error) // f, err := os.Open(filename)
    func WriteFile(filename string, data []byte, perm os.FileMode) error  //os.OpenFile
    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 接口,所以在文件对象上,我们可以直接读写文件。

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

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

    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        b := make([]byte, 1024)
        f, err := os.Open("./tt.txt")
        _, err = f.Read(b)
        f.Close()
    
        if err != nil {
            fmt.Println(err)
        }
        fmt.Println(string(b))
    
    }
    

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

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

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

    type Reader struct {
        buf          []byte
        rd           io.Reader // reader provided by the client
        r, w         int       // buf read and write positions
        err          error
        lastByte     int
        lastRuneSize int
    }
    
    type Writer struct {
        err error
        buf []byte
        n   int
        wr  io.Writer
    }
    
    
    func (b *Reader) Read(p []byte) (n int, err error) 
    func (b *Writer) Write(p []byte) (nn int, err error) 
    

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

    package main
    
    import (
        "bufio"
        "fmt"
        "io"
        "io/ioutil"
        "os"
        "time"
    )
    
    func read1(path string) {
        fi, err := os.Open(path)
        if err != nil {
            panic(err)
        }
        defer fi.Close()
        buf := make([]byte, 1024)
        for {
            n, err := fi.Read(buf)
            if err != nil && err != io.EOF {
                panic(err)
            }
            if 0 == n {
                break
            }
        }
    }
    
    func read2(path string) {
        fi, err := os.Open(path)
        if err != nil {
            panic(err)
        }
        defer fi.Close()
        r := bufio.NewReader(fi)
        buf := make([]byte, 1024)
        for {
            n, err := r.Read(buf)
            if err != nil && err != io.EOF {
                panic(err)
            }
            if 0 == n {
                break
            }
        }
    }
    
    func read3(path string) {
        fi, err := os.Open(path)
        if err != nil {
            panic(err)
        }
        defer fi.Close()
        _, err = ioutil.ReadAll(fi)
    }
    
    func main() {
    
        file := "" //找一个大的文件,如日志文件
        start := time.Now()
        read1(file)
        t1 := time.Now()
        fmt.Printf("Cost time %v\n", t1.Sub(start))
        read2(file)
        t2 := time.Now()
        fmt.Printf("Cost time %v\n", t2.Sub(t1))
        read3(file)
        t3 := time.Now()
        fmt.Printf("Cost time %v\n", t3.Sub(t2))
    }
    

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

    31.3 ioutil包

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

    package main
    
    import (
        "fmt"
        "io/ioutil"
        "os"
    )
    
    func main() {
        fileObj, err := os.Open("./tt.txt")
        defer fileObj.Close()
    
        Contents, _ := ioutil.ReadAll(fileObj)
        fmt.Println(string(contents))
    
        if contents, _ := ioutil.ReadFile("./tt.txt"); err == nil {
            fmt.Println(string(contents))
        }
    
        ioutil.WriteFile("./t3.txt", contents, 0666)
    
    }
    

    31.4 bufio包

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

    package main
    
    import (
        "bufio"
        "fmt"
        "os"
    )
    
    func main() {
        fileObj, _ := os.OpenFile("./tt.txt", os.O_RDWR|os.O_CREATE, 0666)
        defer fileObj.Close()
    
        Rd := bufio.NewReader(fileObj)
        cont, _ := Rd.ReadSlice('#')
        fmt.Println(string(cont))
    
        Wr := bufio.NewWriter(fileObj)
        Wr.WriteString("WriteString writes a ## string.")
        Wr.Flush()
    }
    
    程序输出:
    WriteString writes a #
    

    bufio包中,主要方法如下:

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

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

    package main
    
    import (
        "bufio"
        "fmt"
        "strings"
    )
    
    func main() {
        sr := strings.NewReader("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
        buf := bufio.NewReaderSize(sr, 0) //默认16
        b := make([]byte, 10)
    
        fmt.Println("==", buf.Buffered()) // 0
        S, _ := buf.Peek(5)
        fmt.Printf("%d ==  %q\n", buf.Buffered(), s) // 
        nn, er := buf.Discard(3)
        fmt.Println(nn, er)
    
        for n, err := 0, error(nil); err == nil; {
            fmt.Printf("Buffered:%d ==Size:%d== n:%d==  b[:n] %q ==  err:%v\n", buf.Buffered(), buf.Size(), n, b[:n], err)
            n, err = buf.Read(b)
            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)
        }
    
        fmt.Printf("%d ==  %q\n", buf.Buffered(), s)
    }
    

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

    本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42
    本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963

    虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。
    联系邮箱:roteman@163.com

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/zijtnftx.html