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

《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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • io包中最重要的是两个接口:Reader和Writer Reader接口##### type Writer int...
    勿以浮沙筑高台阅读 16,245评论 0 5
  • 将两个(或更多)语句放在一行书写,它们 必须用分号 (’;’) 分隔。一般情况下,你不需要分号。 init函数和m...
    涵仔睡觉阅读 3,767评论 0 8
  • 一、基础知识:1、JVM、JRE和JDK的区别:JVM(Java Virtual Machine):java虚拟机...
    杀小贼阅读 2,357评论 0 4
  • 周日,正常跑。 两天都是正常跑,因为周末时间错乱,昨晚(1.28)忘了记录,今天(1.29)补上。
    晨晖暮雪阅读 88评论 0 0
  • 静态库:以.a 和 .framework为文件后缀名。动态库:以.tbd(之前叫.dylib) 和 .framew...
    内心强大的Jim阅读 123评论 0 0