使用Golang操作文件的那些事兒

2021-03-02 積跬Coder

Os模塊的使用與源碼研究

文件:計算機中的文件是存儲在外部介質(通常是磁碟)上的數據集合,文件分為文本文件和二進位文件。例如咱們常見的文件後綴名.exe,.txt,'.word'…等等

文件的基本操作可簡單分為增、刪兩類,也就是咱們所說的CURD(增刪改查),也是基於此兩類操作。可簡單理解為打開文件夾、CURD、關閉文件夾。結束~

golang對於文件基本上都是基於Golang的os模塊,那讓我們一起了解一下,那麼Golang是如何對文件進行操作呢。Let's Go~

打開文件

Golang中打開文件使用os.Open模塊,官方os.open部分源碼如下:

// os.Open
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

Open打開命名文件以供讀取。如果成功,則可以使用返回文件上的方法進行讀取;關聯的文件。描述符的模式為O_RDONLY。如果有錯誤,它將是* PathError類型。

它接收一個string 類型的變量name,返回兩個值,File的指針和錯誤error。那麼我們使用它打開文件的的時候就需要這樣做

fileObj, err := os.Open(name string)
// 其中os.Open中的name為路徑Path

基礎使用的介紹暫且為止,其實我們更應該關心的應該是OpenFile(name, O_RDONLY, 0),這個函數到底幹了啥,我們追蹤一下這個函數(在GoLang編輯器中, mac可以直接使用command + 滑鼠左鍵直接進入,Win可以使用ctrl + 滑鼠左鍵),如下:

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    testlog.Open(name)
    f, err := openFileNolog(name, flag, perm)
    if err != nil {
        return nil, err
    }
    f.appendMode = flag&O_APPEND != 0

    return f, nil
}
// OpenFile是廣義的open調用;大多數用戶將使用Open 或Create代替。它打開帶有指定標誌的命名文件(O_RDONLY等)。如果該文件不存在,並且傳遞了O_CREATE標誌,則會使用模式perm(在umask之前)創建該文件。如果成功,返回文件上的方法可以用於I / O。 如果有錯誤,它將是* PathError類型。

這個文件全部內容還是有點分量的,有信息的夥伴,可以詳細的閱讀一下全部內容。暫且為止

那讓我們實踐一下,使用Golang打開文件,如下

package main

import (
    "fmt"
    "os"
)

func main() {
    // 打開此文件,./main.go為相對路徑。在這裡是此文件
    fileObj, err := os.Open("./main.go")
    // 異常處理
    if err != nil {
        fmt.Printf("Open File Error Message:%#v\n", err)
        return
    }
    // 嘗試列印(此處輸出的為地址值)
    fmt.Println(&fileObj)
    // defer 關閉文件
    defer fileObj.Close()
}

以防忘記關閉文件,造成bug,我們在這裡使用defer + 關閉。

注意:在編輯器中並不建議直接使用滑鼠右鍵運行,這樣可能會導致路徑錯誤。大部分的編輯器都並不是只運行此文件!!!

Open File Error Message:&os.PathError{Op:"open", Path:"./main.go", Err:0x2}

如果你遇見了類似的錯誤,你可以直接在終端中,切換到當前路徑。使用go run main.go,直接運行。這樣就可以直接得到正確的結果啦

讀取文件

打開文件之後,那麼我們可以就可以對他們進行操作了,我們在這裡主要演示一下讀取文件的操作。還是老樣子,先看一下主要的相關源碼,如下:

// FileObj.Read()
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

// f.read(b)
func (f *File) read(b []byte) (n int, err error) {
    n, err = f.pfd.Read(b)
    runtime.KeepAlive(f)
    return n, err
}

FileObj.Read()

示例化接受文件的地址值(也就是咱們前面打開獲取到的結果),接受切片的字節,返回讀取的內容,以及錯誤

在此函數中首先檢查是否為有效的讀取,然後在進行f.read(b)的操作,接受其返回結果。

f.read(b)

在這裡,主要檢測是否在讀取,如果是那麼返回本次的讀取內容

從以上我們不難看出,其實讀取文件是讀取文件內部的字節

那麼更具FileObj.Read(),我們可以了解它基本的使用方法,如下

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

讀取部分的示例代碼如下:

在這裡我們需要考慮:是否能夠正常讀取?是否讀完了?具體請看異常處理部分

// 讀取文件
    // 定義每次讀取的大小
    //var tmp = make([]byte, 128)
    var tmp  [128]byte

    // n:從開始到結尾的內容
    n, err := fileObj.Read(tmp[:])
    // 異常處理
    if err != nil {
        fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
        return
    }
    if err == io.EOF {
        fmt.Println("文件讀完了")
        return
    }
    fmt.Printf("讀取了%d個字節\n", n)
    fmt.Printf("讀取到的內容:\n%s",tmp[:])

輸出結果如下:

以上很明顯是並沒有讀完的僅讀取了部分,原始的全部代碼如下

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打開此文件,./main.go為相對路徑。在這裡是此文件
    fileObj, err := os.Open("./main.go")
    // 異常處理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 嘗試列印(此處輸出的為地址值)
    fmt.Println(&fileObj)
    // defer 關閉文件
    defer fileObj.Close()

    // 讀取文件
    // 定義每次讀取的大小
    //var tmp = make([]byte, 128)
    var tmp  [128]byte

    // n:從開始到結尾的內容
    n, err := fileObj.Read(tmp[:])
    // 異常處理
    if err != nil {
        fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
        return
    }
    if err == io.EOF {
        fmt.Println("文件讀完了")
        return
    }
    fmt.Printf("讀取了%d個字節\n", n)
    fmt.Printf("讀取到的內容:\n%s",tmp[:])
}

完整讀取for無線循環讀取

由於以上我們並沒有讀取完整個文件,那麼我需要讀取全部的該怎麼辦呢?一個方法是不斷的讀取下去,然後合併在一起就是完整的內容了,示例代碼如下

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打開此文件,./main.go為相對路徑。在這裡是此文件
    fileObj, err := os.Open("./main.go")
    // 異常處理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 嘗試列印(此處輸出的為地址值)
    fmt.Println(&fileObj)
    // defer 關閉文件
    defer fileObj.Close()
    // 循環讀取文件
    var content []byte
    var tmp = make([]byte, 128)
    for {
        n, err := fileObj.Read(tmp)
        if err == io.EOF {
            fmt.Println("文件讀完了")
            break
        }
        if err != nil {
            fmt.Printf("Read of File Error, ErrorMessage:%#v\n", err)
            return
        }
        content = append(content, tmp[:n]...)
    }
    fmt.Println(string(content))
}

主要的思路為:無限循環去讀取,讀完了之後break掉。然後把讀取的內容合併起來

這種讀取雖然可行,不過是否有點太麻煩了,那麼有什麼更簡便的方式呢?答案當然是有的,bufio讀取

bufio讀取

bufio是在file的基礎上封裝了一層API,支持更多的功能。

主要的部分源碼如下所示

// bufio.NewReader
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
    return NewReaderSize(rd, defaultBufSize)
}

// NewReaderSize
// NewReaderSize returns a new Reader whose buffer has at least the specified
// size. If the argument io.Reader is already a Reader with large enough
// size, it returns the underlying Reader.
func NewReaderSize(rd io.Reader, size int) *Reader {
    // Is it already a Reader?
    b, ok := rd.(*Reader)
    if ok && len(b.buf) >= size {
        return b
    }
    if size < minReadBufferSize {
        size = minReadBufferSize
    }
    r := new(Reader)
    r.reset(make([]byte, size), rd)
    return r
}

它簡便的原因是因為已經幫我們定義了文件的指針,以及它還定義了緩衝區,這樣我們使用它來讀取更加的快與便捷。

bufio.NewReader語法格式

func NewReader(rd io.Reader) *Reader 
// 其中rd為我們打開文件的對象

使用如下

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func main() {
    // 打開此文件,./main.go為相對路徑。在這裡是此文件
    fileObj, err := os.Open("./main.go")
    // 異常處理
    if err != nil {
        fmt.Printf("Open of File Error, ErrorMessage:%#v\n", err)
        return
    }
    // 嘗試列印(此處輸出的為地址值)
    fmt.Println(&fileObj)
    // defer 關閉文件
    defer fileObj.Close()
    // bufio讀取
    reader := bufio.NewReader(fileObj)
    for {
        line, err := reader.ReadString('\n') //注意是字符
        if err == io.EOF {
            if len(line) != 0 {
                fmt.Println(line)
            }
            fmt.Println("文件讀完了")
            break
        }
        if err != nil {
            fmt.Println("read file failed, err:", err)
            return
        }
        fmt.Print(line)
    }
}

輸入結果如上,略。。。

搞了這麼多,就沒有一鍵讀取的麼?當然也是有的,讓我們來了體驗一下ioutil讀取整個文件的愉悅。

package main

import (
    "fmt"
    "io/ioutil"
)

// ioutil.ReadFile讀取整個文件
func main() {
    content, err := ioutil.ReadFile("./main.go")
    if err != nil {
        fmt.Println("read file failed, err:", err)
        return
    }
    fmt.Println(string(content))
}

其內部的實現原理,先預測整個文件的大小。然後一次性全部讀取。當然需要做好異常的準備哦

// ReadFile reads the file named by filename and returns the contents.
// A successful call returns err == nil, not err == EOF. Because ReadFile
// reads the whole file, it does not treat an EOF from Read as an error
// to be reported.
func ReadFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    // It's a good but not certain bet that FileInfo will tell us exactly how much to
    // read, so let's try it but be prepared for the answer to be wrong.
    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {
        // As initial capacity for readAll, use Size + a little extra in case Size
        // is zero, and to avoid another allocation after Read has filled the
        // buffer. The readAll call will read into its allocated internal buffer
        // cheaply. If the size was wrong, we'll either waste some space off the end
        // or reallocate as needed, but in the overwhelmingly common case we'll get
        // it just right.
        if size := fi.Size() + bytes.MinRead; size > n {
            n = size
        }
    }
    return readAll(f, n)
}

文件寫入操作

os.OpenFile()函數能夠以指定模式打開文件,從而實現文件寫入相關功能。

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    ...
}

其中:

name:要打開的文件名 flag:打開文件的模式。模式有以下幾種:

模式含義os.O_WRONLY只寫os.O_CREATE創建文件os.O_RDONLY只讀os.O_RDWR讀寫os.O_TRUNC清空os.O_APPEND追加

perm:文件權限,一個八進位數。r(讀)04,w(寫)02,x(執行)01。

Write和WriteString

func main() {
    file, err := os.OpenFile(test.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    str := "hello"
    file.Write([]byte(str))       //寫入字節切片數據
    file.WriteString("hello") //直接寫入字符串數據
}

bufio.NewWriter

func main() {
    file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
    if err != nil {
        fmt.Println("open file failed, err:", err)
        return
    }
    defer file.Close()
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello") //將數據先寫入緩存
    }
    writer.Flush() //將緩存中的內容寫入文件
}

ioutil.WriteFile

func main() {
    str := "hello"
    err := ioutil.WriteFile("./asd.txt", []byte(str), 0666)
    if err != nil {
        fmt.Println("write file failed, err:", err)
        return
    }
}

so cool~

相關焦點

  • golang下文件鎖的使用
    前言題目是golang下文件鎖的使用,但本文的目的其實是通過golang下的文件鎖的使用方法,來一窺文件鎖背後的機制。
  • Golang入門教程——基本操作篇
    比如當我們打開一個文件的時候,不管文件有沒有打開成功,我們都需要記得關閉文件。但如果文件打開不成功可能就會有異常或者是報錯,如果我們把這些情況全部都考慮到,會變得非常複雜。所以這個時候我們通常都會用defer來執行文件的關閉。要注意的是,defer修飾的代碼會被放入棧中。所以最後會按照先進後出的原則進行執行。
  • 使用Golang快速構建WEB應用
    如果發現問題或者有好的建議請回復我我回及時更正。 1.Abstract在學習web開發的過程中會遇到很多困難,因此寫了一篇類似綜述類的文章。作為路線圖從web開發要素的index出發來介紹golang開發的學習流程以及Example代碼。在描述中多是使用代碼來描述使用方法不會做過多的說明。最後可以方便的copy代碼來實現自己的需求。
  • golang之context使用
    背景golang中並發編程的三種實現方式:chan管道、waitGroup和Context。本篇將重點介紹context的使用,告訴大家基本的使用方式,做到會用。Context概念介紹context譯為上下文,golang在1.6.2的時候還沒有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來處理多個goroutine之間與請求域的數據、取消信號、截止時間等相關操作。
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • gRPC 實操指南(golang)
    •C/S架構的傳輸業務,如股票軟體,每天需要用戶登陸的時候去伺服器拉取最新的數據,或者較簡單的文件傳輸業務,登陸驗證業務,證書業務都可以使用rpc的方式•跨語言開發的項目,比如web業務使用golang進行開發,底層使用cpp或c,部分腳本使用py,跨語言通信可以通過RPC提供的不同語言的開發機制進行實現。
  • Golang 性能分析工具簡要介紹
    這個環境變量,則需要到這個環境變量設置的地址中查找 profile 文件)可以通過 -base 參數來比較一下兩次 profile 文件是否有增長,使用如下命令:go tool pprof -base xxx/goroutine-test xxxx001.pb.gz xxxx002.pb.gzEntering interactive mode
  • GoHub 更新至 0.6,在線 Golang 文檔閱讀
    GoHub 使用 Github REST API v3 提供在線 Golang 文檔閱讀.支持兩種結構:Object 單個文檔項目, 多個 Package 組成Array 文檔項目列表, 每個項目的地址和說明, 每個項目下必須有 golist.json例: 單個文檔項目, 使用 GoDocu 生成.
  • 「Golang」for range 使用方法及避坑指南
    遍歷數組和切片遍歷數組和切片的方式都是一樣的,因為切片的使用概率要大於數組,所以主要講的切片的遍歷,數組可以與其相同方式進行使用,首先 Show me code!下面的代碼信息全部引用自:go/src/cmd/compile/internal/gc/range.go優化後代碼引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/首先提前說一下下面會出現的所有變量名的含義,該含義全部引用自編譯器原始碼。
  • Golang指南:頂級Golang框架、IDE和工具列表
    (點擊尾部閱讀原文前往)原文:https://dzone.com/articles/golang-guide-a-list-of-top-golang-frameworks-ides自推出以來,Google的Go程式語言(Golang)越來越受主流用戶的歡迎。
  • 【Golang】圖解channel之數據結構
    channel被設計用來實現goroutine間的通信,按照golang的設計思想:以通信的方式共享內存。
  • Golang語言標準庫 sync 包的 Once 怎麼使用?
    04踩坑我們已經介紹過 Once 是一個只執行一次操作的對象,假如我們在 Do 方法中再次調用 Do 方法會怎麼樣呢?代碼如下:05總結本文開篇介紹了 Once 的官方定義和使用場景,然後結合示例代碼,介紹了 Once 的基本使用,並通過閱讀源碼,介紹了 Once 的實現原理,最後列舉了一個容易踩的「坑」。
  • Golang入門教程——面向對象篇
    golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。
  • 氣象編程 | 使用python操作Excel文件
    今天使用Python來操作Excel。python操作Excel的庫有很多,大概有xlrd、xlwt、openpyxl、XlsxWriter、xlutils、pandas等。這些庫的操作對xls和xlsx的支持不同,有個只可以操作xls,有的只可以進行讀操作。
  • Go語言(Golang)環境搭建詳解
    壓縮版的就是一個壓縮文件,可以解壓得到裡面的內容,他們的名字類似於:壓縮版我們下載後需要解壓,然後自己移動到要存放的路徑下,並且配置環境變量等信息,相比安裝版來說,比較複雜一些,手動配置的比較多。根據自己的作業系統選擇後,就可以下載開發工具包了,Go語言的官方下載地址是 https://golang.google.cn/dl/ 可以打開選擇版本下載。
  • Golang語言之字符串操作
    如有紕漏,後續會在「Go 語言研究院」https://www.golanghub.cn 修正。如果文章對您有所幫助,還請關注公眾號給予支持。/* * param int64, 進位 * return string */ str2 := strconv.FormatInt(number2, 10) fmt.Printf("%T\n", str2)}字符串操作
  • 從0開始學Golang編程-環境安裝
    首先,在開始之前,我們需要安裝兩個東西:1:Golang https://golang.google.cn/dl/;2: Vs Code https://code.visualstudio.com/我們去Golang官網下載Goalng所需的運行環境,下載完成後,直接雙擊即可安裝完成後,在cmd命令行執行go version
  • Golang語言情懷-第46期 Go 語言標準庫翻譯 compress/bzip2
    功能說明:.bz2文件的壓縮程序。   語  法:bzip2 [-cdfhkLstvVz][--repetitive-best][--repetitive-fast][- 壓縮等級][要壓縮的文件]   補充說明:bzip2採用新的壓縮演算法,壓縮效果比傳統的LZ77/LZ78壓縮演算法來得好。
  • Linux系統隱藏文件/文件夾操作教程
    關於Linux系統的操作方式非常之多,今天小編就來介紹在Linux系統中有效隱藏文件和文件夾的操作教程,以及除了隱藏的東西外,如何在終端以及文件管理器中顯示這些隱藏的項目。目前Linux平臺上的所有文件管理器都可以選擇【查看隱藏文件】,但是這種方法適用於那些更喜歡使用終端的人。要查看這些文件,首先使用cd進入隱藏文件/文件夾的特定目錄。接下來,使用下面的命令來顯示所有文件,無論是可見的還是隱藏的。ls –als命令用於顯示當前目錄中的所有項目,但不顯示隱藏項目, 要查看隱藏的項目,需要-a開關。
  • 文件操作的正確流程,C語言文件操作的函數
    引言操作文件的正確操作流程為:打開文件—>讀寫文件—>關閉文件在對文件進行讀寫操作之前,需要先打開文件,操作完成之後就要關閉文件!所謂的打開文件,就是需要獲取文件的信息,例如文件名、文件狀態以及文件位置;而對於文件的操作,就是對文件的讀(read)與寫(write),C語言對於文件的操作十分的靈活;同時在對文件完成操作之後,就需要關閉文件,不僅是為了禁止對文件的操作,同時也是為釋放儲存文件指針FILE的內存空間資源。