iT邦幫忙

2023 iThome 鐵人賽

DAY 30
0
自我挑戰組

Go in 3o系列 第 30

[Day30] Go in 30 - 系統與檔案 - 檔案讀取與寫入

  • 分享至 

  • xImage
  •  

一、 本篇提要

接下來會說明檔案的建立、和寫入,在此之前會需要先了解什麼是檔案權限。

  • 檔案存取權限
  • 建立與寫入檔案
  • os.OpenFile()

二、檔案存取權限

Go 語言沿用 Unix 系統檔案權限命名法,以符號以及八進位數字來表示。

檔案權限一共有三中 : 讀取(read)、寫入(write)、執行(execute)

權限名稱 符號 八進位值 說明
讀取 r 4 允許檔案的讀取
寫入 w 2 允許檔案的寫入或刪除
執行 x 1 允許或拒絕使用者執行檔案
- 0 沒有給予任何權限

此外,對於每一個檔案,會針對三組不同的個人或群組指定不同權限 :

  1. 擁有人(owner) : 擁有者,又稱 root user
  2. 群組(group) : 通常包括個人或其他群組
  3. 其他(other) : 不屬於以上兩類使用者或群組

https://ithelp.ithome.com.tw/upload/images/20231007/20162693DUfxCh5YjX.jpg

開頭破折號(-)代表這是一個檔案;如果是 d 代表這是個目錄或資料夾(directory),每一組權限表達都採
讀取寫入執行的格式,而且可以改寫成一個八進位數值。

例如 : rw- 代表該組有讀取、寫入、沒有執行權限,並能寫成八進位的數字 6 :

4 (read) + 2 (write) = 6 (read + write)

單一一組權限所有可能組合 :

權限組合 符號 八進位值 說明
讀取寫入執行 rwx 7 有讀取、寫入和執行權限
讀取寫入 rw- 6 有讀取和寫入權限,沒有執行權限
讀取執行 r-x 5 有讀取和執行權限,沒有寫入權限
只讀取 r-- 4 只有讀取權限,沒有寫入和執行權限
寫入執行 -wx 3 有寫入和執行權限,沒有讀取權限
只寫入 -w- 2 只有寫入權限,沒有讀取和執行權限
只執行 --x 1 只有執行權限,沒有讀取和寫入權限
無任何權限 --- 0 沒有讀取、寫入和執行權限

全部三組權限所有可能組合 :
八進位數字(八進位數字以0開頭)
也就是Go語言存取檔案時會用到的代碼。

權限 (全部三組) 符號 八進位
owner: 讀取、group: 讀取、other: 讀取 -r--r--r-- 0444
owner: 寫入、group: 寫入、other: 寫入 -w--w--w-- 0222
owner: 執行、group: 執行、other: 執行 --x--x--x 0111
owner: 讀取、寫入、執行、group: 讀取、寫入、other: 執行 -rwxrw--x 0763
owner: 讀取、寫入、group: 讀取、寫入、other: 讀取、寫入 -rw-rw-rw- 0666
owner: 讀取、寫入、執行、group: 讀取、寫入、執行、other: 讀取、寫入、執行 -rwxrwxrwx 0777

三、用 os 套件新建檔案

3.1 os.Create() 建立檔案

os 套件的 Create() 方法能建立一個新檔案,並賦予權限 0666 (請看上面表格),如果該檔案已經存在那麼該檔案內容會被清空。

func Create(name string) (*file, error)

如果檔案成功興建或清空,os.Create()會回傳一個 *os.File 結果。
(ps. os.File 結構實作了 io.Reader 介面,事實上它同時也實作 io.Writer 介面,這點十分重要,等會提。)

以下程式會先建立一個 test.txt 檔,並在程式結束時以 File 結構的 Close() 關閉 :

package main

import "os"

func main() {
    f, err := os.Create("test.txt") //建立 test.txt 檔
    if err != nil { //檢查建立檔案時是否遇到錯誤
        panic(err) 
    }
    defer f.Close() //確保在 main() 結束時關閉檔案
}

3.2 對檔案寫入字串

建空檔案很簡單,但我們還得對它寫入資料,檔案才有內容。
這時我們可以運用 os.File 的兩個方法 :

Wirte(b []byte) (n int, err error)
WirteString(s string) (n int, err error)

Writer() 和 WriteString() 的功能是一樣的,單接收的參數不一樣,一個接收的是 []byte,另一個是 string。
傳回值 n 代表寫入了 n 個位元,如果寫入失敗會回傳非 nil 。

以下範例 :

package main

import "os"

func main() {
   f, err := os.Create("test.txt")
   if err != nil {
       panic(err)
   }
   defer f.Close()
   f.Write([]byte("使用 Write()寫入 \n"))
   f.WriteString("使用 WriteString()寫入 \n")
}

可以看到目錄底下出現test.txt檔,而且有被寫入 :

https://ithelp.ithome.com.tw/upload/images/20231008/20162693RU1ZcnKd2l.png

3.3 一次完成建立檔案及寫入

Go 語言也允許我們用單一一個指令建立新檔案、並直接完成寫入。
使用 os 套件的 WriteFile() 其定義 :

func WriteFile(filename string, data []byte, perm os.FileMOde) error
  • filename : 檔名,如果檔案不存在就會幫你新建立,如果已存在的檔案則會清空其內容。
  • data ([]byte切片) : 是要寫入的字串
  • perm : 是檔案權限,如前面所說 0666...等,會用來設定新建檔案的權限,如果檔案存在,就不會更改原有權限。

範例 :

package main

import "os"

func main() {
    message := "Hello Golang !"
    // 建檔並寫入
    err := os.WriteFile("test.txt", []byte(message), 0644)
    if err != nil {
        panic(err)
    }
{

輸出寫入結果 :
https://ithelp.ithome.com.tw/upload/images/20231008/20162693zudqxMXuMw.png

3.4 檢查檔案是否存在

在上面介紹的os.Create()、os.WriteFile(),遇到已存在的檔案時,都會毫不留情地清空檔案,但不見得每次我們都需要這樣,所以可以在新建立檔案時,先檢查一下檔案是否存在。

Go 語言檢查檔案存在與否很簡單 :

package main

import (
    "errors"
    "fmt"
    "os"
)

//檢查檔案是否存在的自訂函式
func checkFile(filename string) {
    finfo, err := os.Stat(filename) //取得檔案描述資訊
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            fmt.Printf("%v:檔案不存在!\n\n", filename)
            return
        }
    }
    fmt.Printf("檔名 : %s\n目錄: %t\n修改時間: %v\n權限: %v\n 大小: %d \n\n", finfo.Name() ,finfo.IsDir(), finfo.ModTime(), finfo.Mode(), finfo.Size())
}
 os.Stat()

os.Stat() 方法回傳的錯誤可能包含多重 error 值,我們得檢查是否包含 os.ErrNotExist 錯誤,是的話代表此檔案不存在,我們在範例中檢查的是error值是否包含 os.ErrNotExist。

errors.Is(err, os.ErrNotExist),它的功能是檢查 err 是否是 os.ErrNotExist,或者說 err 是否表示一個檔案或目錄不存在的錯誤。

**關於 os.Stat()以及 os.File 結構 Stat()方法 **,會回傳一個 os.fileStat 結構,它實作了 FileInfo 介面。這個介面可以查到檔案的資訊 :

type FileInfo interface {
    Name() string            // 檔名
    Size() int64             // 檔案大小
    Mode() FileMode          // 修改權限
    ModTime() time.Time      // 修改時間
    IsDir() bool             // 是否為目錄,相當於呼叫 Mode().IsDir()
    Sys() interface{}        // 檔案資料來源 (有可能回傳 nil)
}

3.5 一次讀取整個檔案內容

本節提供兩種方式一次讀取檔案,但這些如果拿來開啟過大的檔案,會消耗大量系統記憶體。

  1. 使用 os.ReadFile()
    全檔案讀取方式 :
func ReadFile(filename string) ([]byte, error)

ReadFile()會開啟檔名參數 filename 指定的檔案,並讀取其內容,成功的話已 []byte 切片形式回傳,err 也回傳 nil。

os.File結構在讀取內容時,如果碰到檔案結尾也會回傳 io.EOF (end of file) 錯誤,但是 ReadFile 既然是讀取整個檔案,就不會回傳 EOF。

package main

import (
	"fmt"
	"os"
)

func main() {
    // 讀取整個檔案內容
    content, err := os.ReadFile("test.txt")
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("檔案內容")
    fmt.Println(string(content))
}

執行結果 :
https://ithelp.ithome.com.tw/upload/images/20231008/201626933kul9ZJLlc.png

  1. 使用 io.ReadAll() 搭配 os.Open()

io.ReadAll() 和 os.ReadFile() 都是用於讀取資料並返回其內容的,但它們的功能和用途有所不同。

os.ReadFile() 是專門用於讀取檔案的。你給它一個檔案路徑,它就打開那個檔案,讀取它的所有內容,然後將內容返回為一個 []byte 。適合於簡單地讀取整個檔案的內容。

io.ReadAll() 是更一般的。它讀取一個 io.Reader 介面提供的所有資料。io.Reader 介面是一個非常強大和通用的介面,在 Go 的標準庫中被廣泛使用。許多不同的物件都實現了這個介面,包括檔案 (os.File)、網路連接、壓縮流、加密流等等。使用 io.ReadAll(),你可以從任何這些來源讀取資料。

func ReadAll(r io.Reader) ([]byte, error)

舉一個例子,你可以使用 os.Open() 打開一個檔案,得到一個 *os.File 物件,然後將它傳遞給 io.ReadAll() 來讀取它的內容。但你也可以將一個 http.Request.Body(它也實現了 io.Reader 介面)傳遞給 io.ReadAll() 來讀取 HTTP 請求的主體。

func Open(name string) (*File, error)

範例 :

package main

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

func main() {

    f, err := os.Open("test.txt") // 開啟檔案
    if err != nil {
        panic(err)
    }
    defer f.Close()
    
    content, err := io.ReadAll(f) // 讀取檔案"整個"內容
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("檔案內容")
    fmt.Println(string(content))
}

3.6 一次讀取一行字串

如果文字檔相當大的話,可以考慮一次讀取檔案的一行。
對此,我們要使用的是 bufio 套件,也就是帶有緩衝區(buffer)的 io 套件。

為了使用 bufio , 第一步是先將檔案結構轉換成 bufio.Reader 結構 :

func NewReader(rd io.Reader) *Reader
func NewReaderSize(rd io.Reader, size int) *Reader

以上兩個函式都接收一個 io.Reader 介面,差別在於 NewReaderSize 多一個參數 size,代表的是緩衝區大小。
如果設為 0,代表使用預設值 4096 (這也是 NewReader() 會使用的緩衝區大小)。

不管檔案有多大,同時間讀進記憶體的就只有緩衝區能容納的字元數而已,
**Go語言允許你指定的最大緩衝區為 64* 1024 (=65536) **

建立了 bufio.Reader 結構後,你就能使用它新增的方法來讀取檔案。
最常用ReaderString() :

func (b *Reader) ReaderString(delim byte) (string, error)

delim (delimiter) 代表分隔符號,通常會設為 \n ,ReadString() 讀到該字元就會停下,將包含該字元的的字傳回傳。要是在讀到該字元之前就碰到檔案結尾,那麼就傳回結尾前的內容及io.EOF(end of file)錯誤。

範例 :

package main

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

func main() {

    f, err := os.Open("test.txt")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    fmt.Println("檔案內容 :")

    //建立 bufio.Reader 結構,緩衝區大小 10
    reader := bufio.NewReaderSize(f, 10)
    
    for {
        // 讀取 reader 直到碰到換行符號 \n
        line, err := reader.ReadString('\n')
        fmt.Print(line)
        if err == io.EOF { //讀到結尾或結束
            break
        }
    }
}

3.7 刪除檔案

os.Remove()函式 :

func Remove(name string) error

四、最完整的檔案開啟與建立 os.OpenFile()

前面所提到的檔案處理方法,已經很受用大部分情境,但是,如果你想要在開啟檔案時有更特定的行為,例如: 限制他只能唯讀或唯寫模式、要附加還是要清空內容,就得使用 os.OpenFile() 。

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

name : 檔名
perm : 權限(八進位)
flag : 他能決定檔案開啟可進行那些操作,os套件定義了一系列相關常數。

const(
    // 必須指定 O_RDONLY、O_WRONLY 或 O_RDWR 之一。
    O_RDONLY int = syscall.O_RDONLY // 以唯讀方式開啟檔案。
    O_WRONLY int = syscall.O_WRONLY // 以唯寫方式開啟檔案。
    O_RDWR int = syscall.O_RDWR // 以讀寫方式開啟檔案。
    
    // 可以透過 | 來連結以下flag
    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 // 開啟檔案時清空內容。
)

範例 :

package main

import (
	"os"
	"time"
)

func main() {
    //建立或開啟檔案 
    f, err := os.OpenFile("ABC.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    f.Write([]byte(time.Now().String() + "\n"))

}

執行結果 :
專案目錄下會新增一個ABC.txt,而且會寫入目前時間字串。

https://ithelp.ithome.com.tw/upload/images/20231008/20162693cS19z9NaJP.png

只要重複執行就會一直增加,這是因為使用了 O_APPEND flag :
https://ithelp.ithome.com.tw/upload/images/20231008/20162693gyDmBcbIJp.png

flag效果說明 :

  1. os.O_CREATE:如果指定的文件不存在,則此標誌會創建一個新文件。如果文件已經存在,它將打開該文件而不進行任何修改。

  2. os.O_APPEND:這個標誌確保在寫入文件時,資料始終追加到文件的末尾,不會覆蓋文件中的任何現有資料。這對於像日誌文件這樣的用途是很有用的,其中新的條目應該繼續添加到文件的末尾。

  3. os.O_WRONLY:這告訴操作系統,你只打算將數據寫入文件,而不是從中讀取數據。如果你嘗試使用這種方式打開的文件進行讀取,則會返回錯誤。

當使用 "|" 符號串連這些標誌時,基本上是告訴 os.OpenFile,希望文件在這三個標誌的所有指定行為下同時打開。

在例子中,這意味著:

如果 "ABC.txt" 不存在,它將被創建。
我們將只寫入文件,而不從中讀取。
所有寫入的資料都將追加到文件的末尾,而不是覆蓋它。


上一篇
[Day29] Go in 30 - 系統與檔案 - flag 與 signals
系列文
Go in 3o30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言