# README
12-5 建立與寫入檔案
12-5-1 用 os 套件新建檔案
os 套件的 Create() 方法能新建一個空白的新檔案,並賦予權限 0666 (所有使用者/群組都可讀/寫)。
如果檔案已經存在,那該檔案的內容會被清空。
func Create(name string)(*File, error)
成功新建或清空檔案後,os.Create() 會傳回一個 *os.File 結構。 os.File 結構實作了 io.Reader 介面:也同時實作了 io.Writer 介面。
例、以下程式會於程式目錄建立一個叫 text.txt 的文字檔,並在程式結束時以 File 結構的 Close() 關閉它:
package main
import "os"
func main() {
f, err := os.Create("text.txt") // 建立文字檔
if err != nil {
panic(err)
}
defer f.Close() // 確保在 main() 結束時關閉檔案
}
12-5-2 對檔案寫入字串
建立空檔案很簡單,但要對它寫入資料,檔案才會有內容。
可以運用 os.File 的兩個方法:
Write(b []byte)(n int, err error)
WriteString(s string)(n int, err error)
Write() 和 WriteString() 功能是一樣的。只是接收的型別不同,一個是接收 []byte 切片、另一個是接收 string。 傳回值 n:代表函式對檔案寫入了 n 個位元,並會在寫入失敗時傳回非 nil 的 error,不過很多時候我們並不會接收這些值。
例、新建檔案後,對該檔案結構寫入一些字串:
package main
import "os"
func main() {
f, err := os.Create("text.txt") // 建立文字檔
if err != nil {
panic(err)
}
defer f.Close() // 確保在 main() 結束時關閉檔案
f.Write([]byte("使用 Write() 寫入\n"))
f.WriteString("使用 WriteString() 寫入\n")
}
執行以上程式,同目錄下會出現 text.txt 其內容會如下:
使用 Write() 寫入
使用 WriteString() 寫入
12-5-3 一次完成建立檔案及寫入
Go 語言也允許用單一一個指令建立新檔案、並直接完成寫資料的動作。
這要用到 os 套件的 WriteFile() 函式,定義如下:
func WriteFile(filename string, data []byte, perm os.FileMode)error
filename(字串):檔案名稱。如果檔案不存在就會新建一個,而已經存在的檔案則會清空其內容。 data []byte:要寫入的字串。 perm:檔案權限。如前面介紹過的 0666、0763,這會用來設定新建檔案的權限。但若檔案已存在,就不會改變原有權限。
12-5-7 刪除檔案
可以使用 os.Remove() 函式:
func Remove(name string)error
在刪除成功時候傳回值為 nil 的 error。
例、以下為一次完成建檔和寫入資料,及刪除檔案的例子:
package main
import "os"
func main() {
msg := "Hello Golang!"
// 建立檔案並寫入資料
err := os.WriteFile("text.txt", []byte(msg), 0644)
if err != nil {
panic(err)
}
// 刪除檔案
rm := os.Remove("test.txt")
if rm != nil {
panic(rm)
}
}
12-5-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) { // 若 error 中包含檔案不存在錯誤
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())
}
func main() {
checkFile("text.txt")
checkFile("junk.txt")
}
結果顯示:
檔名:text.txt
是目錄:false
修改時間:2024-07-30 16:46:54.376146602 +0800 CST
權限:-rw-r--r--
大小:13
junk.txt:檔案不存在!
os.Stat() 方法傳回的錯誤可能包含多重 error 值,我們得檢查當中是否包含 os.ErrNotExist 錯誤,是的話就代表此檔案不存在。 errors.Is(err, os.ErrNotExist) :他的功能是檢查 err 是否是 os.ErrNotExist,或者說 err 否表示一個檔案或目錄不存在的錯誤。 Go 1.13起擴充了錯誤檢查機制,官方建議使用 errors.Is(error, <欲檢查的錯誤值>) 來取代 os.IsNotExist() 等函式。 Go 1.13 之前的版本中,得使用 os.IsNotExist(error) 來檢查 error 值是否包含 os.ErrNotExist 值。
以下為 Go 1.13 版本:
package main
import (
"fmt"
"os"
)
func main() {
finfo, err := os.Stat("junk.txt")
if err != nil {
if os.IsNotExist(err) {
//fmt.Printf("%v:檔案不存在!\n\n", finfo)
fmt.Println(finfo)
}
}
finfo, err = os.Stat("text.txt")
if err != nil && os.IsNotExist(err) {
fmt.Println("text")
}
fmt.Printf("檔名:%s\n是目錄:%t\n修改時間:%v\n權限:%v\n大小:%d\n\n", finfo.Name(), finfo.IsDir(), finfo.ModTime(), finfo.Mode(), finfo.Size())
}
顯示結果:
<nil>
檔名:text.txt
是目錄:false
修改時間:2024-07-30 16:46:54.376146602 +0800 CST
權限:-rw-r--r--
大小:13
os.Stat() 及 os.File 結構的 Stat() 方法,會傳回一個 os.fileStat 結構,塌實做了 FileInfo 介面。
這介面的方法能查詢檔案的各種資訊:
type FileInfo interface{
Name() string // 檔名
Size() int // 檔案大小(計算方式取決於系統)
Mode() FileMode // 修改權限
ModTime() time.Time // 修改時間
IsDir() bool // 是否為目錄,相當等於呼叫 Mode().IsDir()
Sys() interface{} //檔案資料來源(有可能傳回 nil
}
12-5-5 一次讀取整個檔案內容
在建立檔案後,自然會需要讀取它。若檔案不算太大的話,可以用本小節的兩個方式一口氣讀進所有內容。
但若拿來開啟過大的檔案,就會耗掉大量系統的記憶體。下節會看如何一次只讀取一行字的做法。
使用 os.ReadFile()
第一種檔案全讀的方法如下:
func ReadFile(filename string)([]byte, error)
os.ReadFile() 會開啟檔名參數 filename 指定的檔案並讀取其內容,成功的話會以 []byte 切片形式傳回,error 會傳回 nil。 os.File 結構在讀取內容時,或碰到檔案結尾會傳回 io.EOF(end of file)錯誤,但 ReadFile 是讀取整個檔案,故不會傳回 EOF。
package main
import (
"fmt"
"os"
)
func main() {
content, err := os.ReadFile("text.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println("檔案內容:")
fmt.Println(string(content))
}
顯示結果:
檔案內容:
Hello Golang!
使用 io.ReadAll() 搭配 os.Open()
第二種檔案全讀的方法:
func ReadAll(r io.Reader)([]byte, error)
和第一種全讀功能很像,差別在於接收的參數是 io.Reader 介面型別。 這表示 ReadAll() 不只可以用來讀取 os.File 檔案,也能讀取符合 io.Reader 介面的任何物件,如 strings.NewReader() 或 http.Request 等。
若要讀取檔案,得先取得該檔案的 os.File 結構,辦法是使用 os.Open() 函式:
func Open(name string)(*File, error)
package main
import (
"fmt"
"io"
"os"
)
func main() {
f, err := os.Open("text.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))
}
12-5-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() 會使用的緩衝區大小)。
不管檔案有多大,同時間讀進記憶體的就只有緩衝區能容納的字元數而已,這樣一來就能達到節省空間的目的。
btw Go 語言允許你指定的最大緩衝區是 64*1024(=65536)。
建立了 bufio.Reader 結構後,就能使用它新增的方法來讀取檔案。
最常用的叫做 ReadString():
func (b *Reader) ReadString(delim byte)(string, error)
參數 delim:表示分隔符號 delimiter,通常會設為 \n,ReadString() 讀到該字元就會停下來,將包含該字元的字串傳回。 要是讀到該字元前就碰到檔案結尾,那就會傳回結尾前的內容以及 io.EOF(end of file)錯誤。
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("text.txt")
if err != nil {
panic(err)
}
defer file.Close()
fmt.Println("檔案內容:")
// 建立一個 bufio.Reader 結構,緩衝區大小 10
reader := bufio.NewReaderSize(file, 10)
for {
// 讀取 reader 直到碰到換行符號為止(讀取一行文字)
line, err := reader.ReadString('\n')
fmt.Printf("%s\n", line)
if err == io.EOF { // 若已讀到檔案結尾就結束
break
}
}
}
顯示結果:
檔案內容:
Hello Golang!
bufio 套件其實還有很多能讀取檔案的方式,這裡就介紹到這。