接下來會說明檔案的建立、和寫入,在此之前會需要先了解什麼是檔案權限。
Go 語言沿用 Unix 系統檔案權限命名法,以符號以及八進位數字來表示。
檔案權限一共有三中 : 讀取(read)、寫入(write)、執行(execute)
權限名稱 | 符號 | 八進位值 | 說明 |
---|---|---|---|
讀取 | r | 4 | 允許檔案的讀取 |
寫入 | w | 2 | 允許檔案的寫入或刪除 |
執行 | x | 1 | 允許或拒絕使用者執行檔案 |
無 | - | 0 | 沒有給予任何權限 |
此外,對於每一個檔案,會針對三組不同的個人或群組指定不同權限 :
開頭破折號(-)代表這是一個檔案;如果是 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 套件的 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() 結束時關閉檔案
}
建空檔案很簡單,但我們還得對它寫入資料,檔案才有內容。
這時我們可以運用 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檔,而且有被寫入 :
Go 語言也允許我們用單一一個指令建立新檔案、並直接完成寫入。
使用 os 套件的 WriteFile() 其定義 :
func WriteFile(filename string, data []byte, perm os.FileMOde) error
範例 :
package main
import "os"
func main() {
message := "Hello Golang !"
// 建檔並寫入
err := os.WriteFile("test.txt", []byte(message), 0644)
if err != nil {
panic(err)
}
{
輸出寫入結果 :
在上面介紹的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)
}
本節提供兩種方式一次讀取檔案,但這些如果拿來開啟過大的檔案,會消耗大量系統記憶體。
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))
}
執行結果 :
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))
}
如果文字檔相當大的話,可以考慮一次讀取檔案的一行。
對此,我們要使用的是 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
}
}
}
os.Remove()函式 :
func Remove(name string) error
前面所提到的檔案處理方法,已經很受用大部分情境,但是,如果你想要在開啟檔案時有更特定的行為,例如: 限制他只能唯讀或唯寫模式、要附加還是要清空內容,就得使用 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,而且會寫入目前時間字串。
只要重複執行就會一直增加,這是因為使用了 O_APPEND flag :
flag效果說明 :
os.O_CREATE:如果指定的文件不存在,則此標誌會創建一個新文件。如果文件已經存在,它將打開該文件而不進行任何修改。
os.O_APPEND:這個標誌確保在寫入文件時,資料始終追加到文件的末尾,不會覆蓋文件中的任何現有資料。這對於像日誌文件這樣的用途是很有用的,其中新的條目應該繼續添加到文件的末尾。
os.O_WRONLY:這告訴操作系統,你只打算將數據寫入文件,而不是從中讀取數據。如果你嘗試使用這種方式打開的文件進行讀取,則會返回錯誤。
當使用 "|" 符號串連這些標誌時,基本上是告訴 os.OpenFile,希望文件在這三個標誌的所有指定行為下同時打開。
在例子中,這意味著:
如果 "ABC.txt" 不存在,它將被創建。
我們將只寫入文件,而不從中讀取。
所有寫入的資料都將追加到文件的末尾,而不是覆蓋它。