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

xiaohai 2021-05-05 21:46:52 2604人围观 标签: Go 
简介Golang读取文件和处理超大文件

在日常编码中,常常也会遇到如何读取文件,其实读取文件看是简单,但是如果文件是一个特别大的文件,那么如何办呢?本文主要讲解下如何读取文件?

  • 整个文件读取
    • 适用场景:文件较小
  • 按照每行读取
    • 适用场景:如果是大文件,文件内容有严格的分行,可以使用分行读取
  • 按照块读取
    • 使用场景:超大文件

文件准备

$ ll -h
total 462M
-rw-r--r-- 1 zhj 197121  14M 6月   5 13:55 file_1.log
-rw-r--r-- 1 zhj 197121 414M 6月   5 13:54 file_2.log

整文件读取

示例1

常规读取文件操作

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "time"
)

func main() {

    file1 := "./file_1.log"
    file2 := "./file_2.log"

    readFileBuff(file1)
    readFileBuff(file2)
}

//读取文件,常规流程
func readFileBuff(filename string) (content []byte) {
    startTime := time.Now()

    //打开文件
    fileHandler, err := os.Open(filename)
    if err != nil {
        log.Println(err.Error())
        return
    }
    //关闭文件
    defer fileHandler.Close()

    //获取当前文件的信息
    fileInfo, err := fileHandler.Stat()
    if err != nil {
        log.Println(err.Error())
        return
    }

    //初始化切片的长度
    content = make([]byte, fileInfo.Size())

    //读取文件内容到content中
    n, err := fileHandler.Read(content)
    if err != nil {
        log.Println(err.Error())
        return
    }
    fmt.Println("读取的内容长度:", n)
    fmt.Println("运行时间:", time.Now().Sub(startTime))
    return content
}

运行结果:

$ go run main.go
读取的内容长度: 13816352
运行时间: 8.9166ms
读取的内容长度: 433550208
运行时间: 258.3091ms

上面读取文件是我们一个比较常规的操作,但是实际在golang中,已经有包帮我们处理了这个读取,io/ioutil包就是干了这件事,下面示例2就是采用该包读取

示例2

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "time"
)

func main() {

    file1 := "./file_1.mp4" //14M
    file2 := "./file_2.mp4" //414M

    readFile(file1)
    readFile(file2)
}

//读取文件
func readFile(filename string) (content []byte) {
    startTime := time.Now()
    content, err := ioutil.ReadFile(filename)
    if err != nil {
        log.Println(err.Error())
    }
    fmt.Println("读取的内容长度:", len(content))
    fmt.Println("运行时间:", time.Now().Sub(startTime))
    return
}

运行结果:

$ go run main.go
读取的内容长度: 13816352
运行时间: 7.976ms
读取的内容长度: 433550208
运行时间: 291.2078ms

是不是比示例1简单了很多,可以去看readFile的实现,内部的大致流程就是按照示例1去实现的。

分片读取

当一个文件是非常大,如20G的日志文件,我们按照上面的整个文件读取,其实是不现实的,可能内存都没有这么大,那么我们就要考虑分段读取。

分段读取的思路:

  • 设置一个容量的切片
  • 每次都读取固定长度的内容到切片中
  • 直到文件内容读取完为止
package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)

func main() {

    file1 := "./file_1.log"
    file2 := "./file_2.log"

    readBlock(file1)
    readBlock(file2)
}

//分片读取
func readBlock(filename string) (content []byte) {
    startTime := time.Now()

    //打开文件
    fileHandler, err := os.Open(filename)
    if err != nil {
        log.Println(err.Error())
        return
    }
    //关闭文件
    defer fileHandler.Close()
    buffer := make([]byte, 1024)
    for {
        n, err := fileHandler.Read(buffer)
        if err != nil && err != io.EOF {
            log.Println(err.Error())
        }
        //读取完成
        if n == 0 {
            break
        }
        content = append(content, buffer[:n]...)
    }
    fmt.Println("读取的内容长度:", len(content))
    fmt.Println("运行时间:", time.Now().Sub(startTime))
    return
}

运行结果:

$ go run main.go
读取的内容长度: 13816352
运行时间: 83.8145ms
读取的内容长度: 433550208
运行时间: 2.5322629s

逐行读取

逐行读取适合大文件,逐行读取要求文件里面的内容一定是分行存储的,并且每行的内容不能过大。

方式一:

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)

func main() {
    file1 := "./file_1.log"
    file2 := "./file_2.log"
    readLine(file1)
    readLine(file2)
}

func readLine(filename string) (content []byte) {
    startTime := time.Now()

    //打开文件
    fileHandler, err := os.Open(filename)
    if err != nil {
        log.Println(err.Error())
        return
    }
    //关闭文件
    defer fileHandler.Close()

    lineReader := bufio.NewReader(fileHandler)
    for {
        line, _, err := lineReader.ReadLine()
        if err != nil && err == io.EOF {
            break
        }
        content = append(content, line...)
    }

    fmt.Println("读取的内容长度:", len(content))
    fmt.Println("运行时间:", time.Now().Sub(startTime))

    return
}

运行结果:

$ go run main.go
读取的内容长度: 13755104
运行时间: 57.8427ms
读取的内容长度: 431320344
运行时间: 1.7922053s

方法二:

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "time"
)

func main() {
    file1 := "./file_1.log"
    file2 := "./file_2.log"
    readScanner(file1)
    readScanner(file2)
}

func readScanner(filename string) (content []byte) {
    startTime := time.Now()

    //打开文件
    fileHandler, err := os.Open(filename)
    if err != nil {
        log.Println(err.Error())
        return
    }
    //关闭文件
    defer fileHandler.Close()
    lineScanner := bufio.NewScanner(fileHandler)
    for lineScanner.Scan() {
        content = append(content, lineScanner.Bytes()...)
    }

    fmt.Println("读取的内容长度:", len(content))
    fmt.Println("运行时间:", time.Now().Sub(startTime))

    return
}

执行结果:

$ go run main.go
读取的内容长度: 13755104
运行时间: 51.8617ms
读取的内容长度: 431320344
运行时间: 1.623657s

总结:

  • 从以上三种读取文件的方式可以看出,整个文件读取的效率是非常高的,但是这种方式只适用于小文件,大文件这样读取可能造成内存溢出
  • 如果文件内容特别大,最好使用分片读取和逐行读取,但是逐行读取的文件要注意每行的内容不能太大,否则也会出现问题
  • 二进制文件适合使用整个文件读取和分片读取
  • 对文件内容需要处理最好选用分片读取和逐行读取