Golang读取文件和处理超大文件方案

Golang 操作文件的读取的方法很多,适用的场景也是各不相同,在此我们将文件的读取分为如下几种 :

  1. 文件整体读取
  2. 文件分片读取(块级读取)
  3. 文件行级读取

系统的配置不同,执行的耗时也不相同,此处给出一参考

系统配置 :

OS : Windows10

Memory : 16G

CPU (英特尔)Intel(R) Core(TM) i3-4370 CPU @ 3.80GHz(3800 MHz)

1. 文件整体读取

文件整体读取就是将文件一次性读取到,理解上是将文件的内容第一次就读取完了

使用场景 :

  1. 针对小文件比较适用(大文件读取空间和时间的消耗也很大)
  2. 对于只读类型的文件也比较适用(文件也不能太大)
代码示例1
package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)
// 测试用的文本文件11M大小
var m11 string = `G:\runtime\log\ccapi\11M.log`
// 测试用的文本文件400M大小
var m400 string = `G:\runtime\log\ccapi\400M.log`

// 将整个文件都读取
func readAll(filePath string) {
    start1 := time.Now()
    ioutil.ReadFile(filePath)
    fmt.Println("readAll spend : ", time.Now().Sub(start1))
}
func main() {
    readAll(m11)
    readAll(m400)
}
$ go run main.go
readAll spend :  6.9999ms
readAll spend :  358.8014ms
代码示例2
package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)
// 测试用的文本文件11M大小
var m11 string = `G:\runtime\log\ccapi\11M.log`
// 测试用的文本文件400M大小
var m400 string = `G:\runtime\log\ccapi\400M.log`
// 将文件完整读取
func readAllBuff(filePath string) {
    start1 := time.Now()
    // 打开文件
    FileHandle, err := os.Open(filePath)
    if err != nil {
        log.Println(err)
        return
    }
    // 关闭文件
    defer FileHandle.Close()
    // 获取文件当前信息
    fileInfo, err := FileHandle.Stat()
    if err != nil {
        log.Println(err)
        return
    }
    buffer := make([]byte, fileInfo.Size())
    // 读取文件内容,并写入buffer中
    n, err := FileHandle.Read(buffer)
    if err != nil {
        log.Println(err)
    }
    // 打印所有切片中的内容
    fmt.Println(string(buffer[:n]))
    fmt.Println("readAllBuff spend : ", time.Now().Sub(start1))
}
func main() {
    readAllBuff(m11)
    readAllBuff(m400)
}

2. 文件分片读取

对文件一部分一部分逐步的读取,直到文件完全读取完

PS : 每次读取文件的大小是根据设置的 分片 大小 ,所以对于读取文本类型的文件时(例如 : 日志文件)
不一定是按照你的期望逐行输出,因为不会处理文本尾部的换行符,而是按照分片大小读取内容

使用场景 :

  1. 读取超大的文件
  2. 读二进制类型的文件(比如:音视频文件或者资源类型文件等)
代码示例
package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)
// 测试用的文本文件11M大小
var m11 string = `G:\runtime\log\ccapi\11M.log`
// 测试用的文本文件400M大小
var m400 string = `G:\runtime\log\ccapi\400M.log`
// 文件一块一块的读取
func readBlock(filePath string) {
    start1 := time.Now()
    FileHandle, err := os.Open(filePath)
    if err != nil {
        log.Println(err)
        return
    }
    defer FileHandle.Close()
    // 设置每次读取字节数
    buffer := make([]byte, 1024)
    for {
        n, err := FileHandle.Read(buffer)
        // 控制条件,根据实际调整
        if err != nil && err != io.EOF {
            log.Println(err)
        }
        if n == 0 {
            break
        }
        // 如下代码打印出每次读取的文件块(字节数)
        //fmt.Println(string(buffer[:n]))
    }
    fmt.Println("readBolck spend : ", time.Now().Sub(start1))
}
func main() {
    readBlock(m11)
    readBlock(m400)
}
$ go run main.go
readBolck spend :  31.9814ms
readBolck spend :  1.0889488s

3. 文件逐行读取

对文件一行一行的读取,直到读到文件末尾

使用场景 :

  1. 读取超大的文件很合适(例如 : 超大文本文件等)
  2. 读取的文件最好是有换行的(如果使用单行文件组成的大文件,需要注意)
  3. 对需要分析的内容大文件
    1. 统计某些数据出现的次数
    2. 查询某些数据是否存在
    3. 查找指定行的数据
示例代码1
package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)
// 测试用的文本文件11M大小
var m11 string = `G:\runtime\log\ccapi\11M.log`
// 测试用的文本文件400M大小
var m400 string = `G:\runtime\log\ccapi\400M.log`
// 读取文件的每一行
func readEachLineReader(filePath string) {
    start1 := time.Now()
    FileHandle, err := os.Open(filePath)
    if err != nil {
        log.Println(err)
        return
    }
    defer FileHandle.Close()
    lineReader := bufio.NewReader(FileHandle)
    for {
        // 相同使用场景下可以采用的方法
        // func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
        // func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
        // func (b *Reader) ReadString(delim byte) (line string, err error)
        line, _, err := lineReader.ReadLine()
        if err == io.EOF {
            break
        }
        // 如下是某些业务逻辑操作
        // 如下代码打印每次读取的文件行内容
        fmt.Println(string(line))
    }
    fmt.Println("readEachLineReader spend : ", time.Now().Sub(start1))
}
func main(){
    readEachLineReader(m11)
    readEachLineReader(m400)
}
$ go run main.go
readEachLineReader spend :  16.9902ms
readEachLineReader spend :  537.9683ms

代码示例2
package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)
// 测试用的文本文件11M大小
var m11 string = `G:\runtime\log\ccapi\11M.log`
// 测试用的文本文件400M大小
var m400 string = `G:\runtime\log\ccapi\400M.log`
// 读取文件的每一行
func readEachLineScanner(filePath string) {
    start1 := time.Now()
    FileHandle, err := os.Open(filePath)
    if err != nil {
        log.Println(err)
        return
    }
    defer FileHandle.Close()
    lineScanner := bufio.NewScanner(FileHandle)
    for lineScanner.Scan() {
        // 相同使用场景下可以使用如下方法
        // func (s *Scanner) Bytes() []byte
        // func (s *Scanner) Text() string
        // 实际逻辑 : 对读取的内容进行某些业务操作
        // 如下代码打印每次读取的文件行内容
        fmt.Println(lineScanner.Text())
    }
    fmt.Println("readEachLineScanner spend : ", time.Now().Sub(start1))
}
func main() {
    readEachLineScanner(m11)
    readEachLineScanner(m400)
}
$ go run main.go
readEachLineScanner spend :  17.9895ms
readEachLineScanner spend :  574.1722ms

4. 总结

  1. 面试中常见的类似超大文件读取的问题,通常我们采用分片读取或者逐行读取的方案即可

    1. 大文件的上传也可以采用类似的解决方案 , 每次读取文件的部分内容上传(写入)网络接口中,直至文件读取完毕
  2. 普通的小文件并且对内容没有太多操作的,可以采用整体读取,速度相对较快

  3. 对文件内容有操作的采用分片读取和逐行读取更合适

  4. 二进制类型文件采用分片读取或者整体读取的方案比较合适

  5. 文件读取不仅是本地文件,要读去网络上的文件(各种文档,音视频,图片,和其他各种类型文件)时要访问到文件获取 io.ReadCloser 或者 io.Reader 后可以采用三种方式将文件内容读取到

    1. func ReadAll(r io.Reader) ([]byte, error) 文件完整读取

    2. func Copy(dst Writer, src Reader) (written int64, err error) 文件读取并写入

    3. type Reader interface {
          Read(p []byte) (n int, err error)
      }
      

      通过Reader 接口的 Read 方法读取

有疏漏 , 万望指正 , 不胜感谢

5. 参考资料

- [1] Golang文档

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