超全總結:Go 語言如何操作文件

前言

哈嘍,大家好,我是asong

我們都知道在Unix中萬物都被稱爲文件,文件處理是一個非常常見的問題,所以本文就總結了Go語言操作文件的常見方式,整體思路如下:

Go 語言版本:1.18

本文所有代碼已經上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/file_operate_demo

操作文件包括哪些

操作一個文件離不開這幾個動作:

所以本文就針對這些操作總結了一些示例方法供大家參考;

Go 語言操作文件可使用的庫

Go 語言官方庫:osio/ioutilbufio涵蓋了文件操作的所有場景,

os提供了對文件IO直接調用的方法,bufio提供緩衝區操作文件的方法,io/ioutil也提供對文件IO直接調用的方法,不過 Go 語言在Go1.16版本已經棄用了io/ioutil庫,這個io/ioutil包是一個定義不明確且難以理解的東西集合。該軟件包提供的所有功能都已移至其他軟件包,所以io/ioutil中操作文件的方法都在io庫有相同含義的方法,大家以後在使用到 ioutil 中的方法是可以通過註釋在其他包找到對應的方法。

文件的基礎操作

這裏我把 創建文件、打開文件、關閉文件、改變文件權限這些歸爲對文件的基本操作,對文件的基本操作直接使用os庫中的方法即可,因爲我們需要進行IO操作,來看下面的例子:

import (
 "log"
 "os"
)
func main() {
 // 創建文件
 f, err := os.Create("asong.txt")
 if err != nil{
  log.Fatalf("create file failed err=%s\n", err)
 }
 // 獲取文件信息
 fileInfo, err := f.Stat()
 if err != nil{
  log.Fatalf("get file info failed err=%s\n", err)
 }

 log.Printf("File Name is %s\n", fileInfo.Name())
 log.Printf("File Permissions is %s\n", fileInfo.Mode())
 log.Printf("File ModTime is %s\n", fileInfo.ModTime())

 // 改變文件權限
 err = f.Chmod(0777)
 if err != nil{
  log.Fatalf("chmod file failed err=%s\n", err)
 }

 // 改變擁有者
 err = f.Chown(os.Getuid(), os.Getgid())
 if err != nil{
  log.Fatalf("chown file failed err=%s\n", err)
 }

 // 再次獲取文件信息 驗證改變是否正確
 fileInfo, err = f.Stat()
 if err != nil{
  log.Fatalf("get file info second failed err=%s\n", err)
 }
 log.Printf("File change Permissions is %s\n", fileInfo.Mode())

 // 關閉文件
 err = f.Close()
 if err != nil{
  log.Fatalf("close file failed err=%s\n", err)
 }
 
 // 刪除文件
 err = os.Remove("asong.txt")
 if err != nil{
  log.Fatalf("remove file failed err=%s\n", err)
 }
}

寫文件

快寫文件

os/ioutil包都提供了WriteFile方法可以快速處理創建 / 打開文件 / 寫數據 / 關閉文件,使用示例如下:

func writeAll(filename string) error {
 err := os.WriteFile("asong.txt"[]byte("Hi asong\n"), 0666)
 if err != nil {
  return err
 }
 return nil
}

按行寫文件

osbuffo寫數據都沒有提供按行寫入的方法,所以我們可以在調用os.WriteStringbufio.WriteString方法是在數據中加入換行符即可,來看示例:

import (
 "bufio"
 "log"
 "os"
)
// 直接操作IO
func writeLine(filename string) error {
 data := []string{
  "asong",
  "test",
  "123",
 }
 f, err := os.OpenFile(filename, os.O_WRONLY, 0666)
 if err != nil{
  return err
 }

 for _, line := range data{
  _,err := f.WriteString(line + "\n")
  if err != nil{
   return err
  }
 }
 f.Close()
 return nil
}
// 使用緩存區寫入
func writeLine2(filename string) error {
 file, err := os.OpenFile(filename, os.O_WRONLY, 0666)
 if err != nil {
  return err
 }

 // 爲這個文件創建buffered writer
 bufferedWriter := bufio.NewWriter(file)
 
 for i:=0; i < 2; i++{
  // 寫字符串到buffer
  bytesWritten, err := bufferedWriter.WriteString(
   "asong真帥\n",
  )
  if err != nil {
   return err
  }
  log.Printf("Bytes written: %d\n", bytesWritten)
 }
 // 寫內存buffer到硬盤
 err = bufferedWriter.Flush()
 if err != nil{
  return err
 }

 file.Close()
 return nil
}

偏移量寫入

某些場景我們想根據給定的偏移量寫入數據,可以使用os中的writeAt方法,例子如下:

import "os"

func writeAt(filename string) error {
 data := []byte{
  0x41, // A
  0x73, // s
  0x20, // space
  0x20, // space
  0x67, // g
 }
 f, err := os.OpenFile(filename, os.O_WRONLY, 0666)
 if err != nil{
  return err
 }
 _, err = f.Write(data)
 if err != nil{
  return err
 }

 replaceSplace := []byte{
  0x6F, // o
  0x6E, // n
 }
 _, err = f.WriteAt(replaceSplace, 2)
 if err != nil{
  return err
 }
 f.Close()
 return nil
}

緩存區寫入

os庫中的方法對文件都是直接的IO操作,頻繁的IO操作會增加CPU的中斷頻率,所以我們可以使用內存緩存區來減少IO操作,在寫字節到硬盤前使用內存緩存,當內存緩存區的容量到達一定數值時在寫內存數據 buffer 到硬盤,bufio就是這樣示一個庫,來個例子我們看一下怎麼使用:

import (
 "bufio"
 "log"
 "os"
)

func writeBuffer(filename string) error {
 file, err := os.OpenFile(filename, os.O_WRONLY, 0666)
 if err != nil {
  return err
 }

 // 爲這個文件創建buffered writer
 bufferedWriter := bufio.NewWriter(file)

 // 寫字符串到buffer
 bytesWritten, err := bufferedWriter.WriteString(
  "asong真帥\n",
 )
 if err != nil {
  return err
 }
 log.Printf("Bytes written: %d\n", bytesWritten)

 // 檢查緩存中的字節數
 unflushedBufferSize := bufferedWriter.Buffered()
 log.Printf("Bytes buffered: %d\n", unflushedBufferSize)

 // 還有多少字節可用(未使用的緩存大小)
 bytesAvailable := bufferedWriter.Available()
 if err != nil {
  return err
 }
 log.Printf("Available buffer: %d\n", bytesAvailable)
 // 寫內存buffer到硬盤
 err = bufferedWriter.Flush()
 if err != nil{
  return err
 }

 file.Close()
 return nil
}

讀文件

讀取全文件

有兩種方式我們可以讀取全文件:

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

func readAll(filename string) error {
 data, err := os.ReadFile(filename)
 if err != nil {
  return err
 }
 log.Printf("read %s content is %s", filename, data)
 return nil
}

func ReadAll2(filename string) error {
 file, err := os.Open("asong.txt")
 if err != nil {
  return err
 }

 content, err := ioutil.ReadAll(file)
 log.Printf("read %s content is %s\n", filename, content)

 file.Close()
 return nil
}

逐行讀取

os庫中提供了Read方法是按照字節長度讀取,如果我們想要按行讀取文件需要配合bufio一起使用,bufio中提供了三種方法ReadLineReadBytes("\n")ReadString("\n")可以按行讀取數據,下面我使用ReadBytes("\n")來寫個例子:

func readLine(filename string) error {
 file, err := os.OpenFile(filename, os.O_RDONLY, 0666)
 if err != nil {
  return err
 }
 bufferedReader := bufio.NewReader(file)
 for {
  // ReadLine is a low-level line-reading primitive. Most callers should use
  // ReadBytes('\n') or ReadString('\n') instead or use a Scanner.
  lineBytes, err := bufferedReader.ReadBytes('\n')
  bufferedReader.ReadLine()
  line := strings.TrimSpace(string(lineBytes))
  if err != nil && err != io.EOF {
   return err
  }
  if err == io.EOF {
   break
  }
  log.Printf("readline %s every line data is %s\n", filename, line)
 }
 file.Close()
 return nil
}

按塊讀取文件

有些場景我們想按照字節長度讀取文件,這時我們可以如下方法:

// use bufio.NewReader
func readByte(filename string) error {
 file, err := os.OpenFile(filename, os.O_RDONLY, 0666)
 if err != nil {
  return err
 }
 // 創建 Reader
 r := bufio.NewReader(file)

 // 每次讀取 2 個字節
 buf := make([]byte, 2)
 for {
  n, err := r.Read(buf)
  if err != nil && err != io.EOF {
   return err
  }

  if n == 0 {
   break
  }
  log.Printf("writeByte %s every read 2 byte is %s\n", filename, string(buf[:n]))
 }
 file.Close()
 return nil
}

// use os
func readByte2(filename string) error{
 file, err := os.OpenFile(filename, os.O_RDONLY, 0666)
 if err != nil {
  return err
 }

 // 每次讀取 2 個字節
 buf := make([]byte, 2)
 for {
  n, err := file.Read(buf)
  if err != nil && err != io.EOF {
   return err
  }

  if n == 0 {
   break
  }
  log.Printf("writeByte %s every read 2 byte is %s\n", filename, string(buf[:n]))
 }
 file.Close()
 return nil
}


// use os and io.ReadAtLeast
func readByte3(filename string) error{
 file, err := os.OpenFile(filename, os.O_RDONLY, 0666)
 if err != nil {
  return err
 }

 // 每次讀取 2 個字節
 buf := make([]byte, 2)
 for {
  n, err := io.ReadAtLeast(file, buf, 0)
  if err != nil && err != io.EOF {
   return err
  }

  if n == 0 {
   break
  }
  log.Printf("writeByte %s every read 2 byte is %s\n", filename, string(buf[:n]))
 }
 file.Close()
 return nil
}

分隔符讀取

bufio包中提供了Scanner掃描器模塊,它的主要作用是把數據流分割成一個個標記併除去它們之間的空格,他支持我們定製Split函數做爲分隔函數,分隔符可以不是一個簡單的字節或者字符,我們可以自定義分隔函數,在分隔函數實現分隔規則以及指針移動多少,返回什麼數據,如果沒有定製Split函數,那麼就會使用默認ScanLines作爲分隔函數,也就是使用換行作爲分隔符,bufio中還提供了默認方法ScanRunesScanWrods,下面我們用SacnWrods方法寫個例子,獲取用空格分隔的文本:

func readScanner(filename string) error {
 file, err := os.OpenFile(filename, os.O_RDONLY, 0666)
 if err != nil {
  return err
 }

 scanner := bufio.NewScanner(file)
 // 可以定製Split函數做分隔函數
 // ScanWords 是scanner自帶的分隔函數用來找空格分隔的文本字
 scanner.Split(bufio.ScanWords)
 for {
  success := scanner.Scan()
  if success == false {
   // 出現錯誤或者EOF是返回Error
   err = scanner.Err()
   if err == nil {
    log.Println("Scan completed and reached EOF")
    break
   } else {
    return err
   }
  }
  // 得到數據,Bytes() 或者 Text()
  log.Printf("readScanner get data is %s", scanner.Text())
 }
 file.Close()
 return nil
}

打包 / 解包

Go 語言的archive包中提供了tarzip兩種打包 / 解包方法,這裏以zip的打包 / 解包爲例子:

zip解包示例:

import (
 "archive/zip"
 "fmt"
 "io"
 "log"
 "os"
)

func main()  {
 // Open a zip archive for reading.
 r, err := zip.OpenReader("asong.zip")
 if err != nil {
  log.Fatal(err)
 }
 defer r.Close()
 // Iterate through the files in the archive,
 // printing some of their contents.
 for _, f := range r.File {
  fmt.Printf("Contents of %s:\n", f.Name)
  rc, err := f.Open()
  if err != nil {
   log.Fatal(err)
  }
  _, err = io.CopyN(os.Stdout, rc, 68)
  if err != nil {
   log.Fatal(err)
  }
  rc.Close()
 }
}

zip打包示例:

func writerZip()  {
 // Create archive
 zipPath := "out.zip"
 zipFile, err := os.Create(zipPath)
 if err != nil {
  log.Fatal(err)
 }

 // Create a new zip archive.
 w := zip.NewWriter(zipFile)
 // Add some files to the archive.
 var files = []struct {
  Name, Body string
 }{
  {"asong.txt""This archive contains some text files."},
  {"todo.txt""Get animal handling licence.\nWrite more examples."},
 }
 for _, file := range files {
  f, err := w.Create(file.Name)
  if err != nil {
   log.Fatal(err)
  }
  _, err = f.Write([]byte(file.Body))
  if err != nil {
   log.Fatal(err)
  }
 }
 // Make sure to check the error on Close.
 err = w.Close()
 if err != nil {
  log.Fatal(err)
 }
}

總結

本文歸根結底是介紹osiobufio這些包如何操作文件,因爲Go語言操作提供了太多了方法,藉着本文全都介紹出來,在使用的時候可以很方便的當作文檔查詢,如果你問用什麼方法操作文件是最優的方法,這個我也沒法回答你,需要根據具體場景分析的,如果這些方法你都知道了,在寫一個 benchmark 對比一下就可以了,實踐纔是檢驗真理的唯一標準。

本文所有代碼已經上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/file_operate_demo

好啦,本文到這裏就結束了,我是 asong,我們下期見。

Golang 夢工廠 asong 是一名後端程序員,目前就職於一家電商公司,專注於 Golang 技術,定期分享 Go 語言、MySQL、Redis、Elasticsearch、計算機基礎、微服務架構設計、面試等知識。這裏不僅有技術,還有故事!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/r30z2i_sSuRfEnM_23wFlg