文件
Go 語言提供文件處理的標准庫大致以下幾個:
os庫,負責 OS 文件系統交互的具體實現io庫,讀寫 IO 的抽象層fs庫,文件系統的抽象層
本文會講解如何通過 Go 語言來進行基本的文件處理。
打開
常見的兩種打開文件的方式是使用os包提供的兩個函數,Open函數返回值一個文件指針和一個錯誤,
func Open(name string) (*File, error)後者OpenFile能夠提供更加細粒度的控制,函數Open就是對OpenFile函數的一個簡單封裝。
func OpenFile(name string, flag int, perm FileMode) (*File, error)先來介紹第一種使用方法,直接提供對應的文件名即可,代碼如下
func main() {
file, err := os.Open("README.txt")
fmt.Println(file, err)
}文件的查找路徑默認為項目go.mod文件所在的路徑,由於項目下並沒有文件README.txt,所以自然會返回一個錯誤。
<nil> open README.txt: The system cannot find the file specified.因為 IO 錯誤的類型有很多,所以需要手動的去判斷文件是否存在,同樣的os包也為此提供了方便函數,修改後的代碼如下
func main() {
file, err := os.Open("README.txt")
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件讀取成功", file)
}
}再次運行輸出如下
文件不存在事實上第一種函數讀取的文件僅僅只是只讀的,無法被修改
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}通過OpenFile函數可以控制更多細節,例如修改文件描述符和文件權限,關於文件描述符,os包下提供了以下常量以供使用。
const (
// 只讀,只寫,讀寫 三種必須指定一個
O_RDONLY int = syscall.O_RDONLY // 以只讀的模式打開文件
O_WRONLY int = syscall.O_WRONLY // 以只寫的模式打開文件
O_RDWR int = syscall.O_RDWR // 以讀寫的模式打開文件
// 剩余的值用於控制行為
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 // 以同步IO的方式打開文件
O_TRUNC int = syscall.O_TRUNC // 當打開的時候截斷可寫的文件
)關於文件權限的則提供了以下常量。
const (
ModeDir = fs.ModeDir // d: 目錄
ModeAppend = fs.ModeAppend // a: 只能添加
ModeExclusive = fs.ModeExclusive // l: 專用
ModeTemporary = fs.ModeTemporary // T: 臨時文件
ModeSymlink = fs.ModeSymlink // L: 符號鏈接
ModeDevice = fs.ModeDevice // D: 設備文件
ModeNamedPipe = fs.ModeNamedPipe // p: 具名管道 (FIFO)
ModeSocket = fs.ModeSocket // S: Unix 域套接字
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: Unix 字符設備, 前提是設置了 ModeDevice
ModeSticky = fs.ModeSticky // t: 黏滯位
ModeIrregular = fs.ModeIrregular // ?: 非常規文件
// 類型位的掩碼. 對於常規文件而言,什麼都不會設置.
ModeType = fs.ModeType
ModePerm = fs.ModePerm // Unix 權限位, 0o777
)下面是一個以讀寫模式打開一個文件的代碼例子,權限為0666,表示為所有人都可以對該文件進行讀寫,且不存在時會自動創建。
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if os.IsNotExist(err) {
fmt.Println("文件不存在")
} else if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件打開成功", file.Name())
file.Close()
}
}輸出如下
文件打開成功 README.txt倘若只是想獲取該文件的一些信息,並不想讀取該文件,可以使用os.Stat()函數進行操作,代碼示例如下
func main() {
fileInfo, err := os.Stat("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(fmt.Sprintf("%+v", fileInfo))
}
}輸出如下
&{name:README.txt FileAttributes:32 CreationTime:{LowDateTime:3603459389 HighDateTime:31016791} LastAccessTime:{LowDateTime:3603459389 HighDateTime:31016791} LastWriteTime:{LowDateTime:3603459389 HighDateTime:31016791} FileSizeHigh
:0 FileSizeLow:0 Reserved0:0 filetype:0 Mutex:{state:0 sema:0} path:README.txt vol:0 idxhi:0 idxlo:0 appendNameToPath:false}WARNING
打開一個文件後永遠要記得關閉該文件,通常關閉操作會放在defer語句裡
defer file.Close()讀取
當成功的打開文件後,便可以進行讀取操作了,關於讀取文件的操作,*os.File類型提供了以下幾個公開的方法
// 將文件讀進傳入的字節切片
func (f *File) Read(b []byte) (n int, err error)
// 相較於第一種可以從指定偏移量讀取
func (f *File) ReadAt(b []byte, off int64) (n int, err error)大多數情況第一種使用的較多。針對於第一種方法,需要自行編寫邏輯來進行讀取時切片的動態擴容,代碼如下
func ReadFile(file *os.File) ([]byte, error) {
buffer := make([]byte, 0, 512)
for {
// 當容量不足時
if len(buffer) == cap(buffer) {
// 擴容
buffer = append(buffer, 0)[:len(buffer)]
}
// 繼續讀取文件
offset, err := file.Read(buffer[len(buffer):cap(buffer)])
// 將已寫入的數據歸入切片
buffer = buffer[:len(buffer)+offset]
// 發生錯誤時
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
}
return buffer, err
}
}
}剩余邏輯如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件打開成功", file.Name())
bytes, err := ReadFile(file)
if err != nil {
fmt.Println("文件讀取異常", err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}輸出為
文件打開成功 README.txt
hello world!除此之外,還可以使用兩個方便函數來進行文件讀取,分別是os包下的ReadFile函數,以及io包下的ReadAll函數。對於os.ReadFile而言,只需要提供文件路徑即可,而對於io.ReadAll而言,則需要提供一個io.Reader類型的實現,
os.ReadFile
func ReadFile(name string) ([]byte, error)使用例子如下
func main() {
bytes, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}輸出如下
hello world!io.ReadAll
func ReadAll(r Reader) ([]byte, error)使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件打開成功", file.Name())
bytes, err := io.ReadAll(file)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
file.Close()
}
}文件打開成功 README.txt
hello world!寫入
os.File結構體提供了以下幾種方法以供寫入數據
// 寫入字節切片
func (f *File) Write(b []byte) (n int, err error)
// 寫入字符串
func (f *File) WriteString(s string) (n int, err error)
// 從指定位置開始寫,當以os.O_APPEND模式打開時,會返回錯誤
func (f *File) WriteAt(b []byte, off int64) (n int, err error)如果想要對一個文件寫入數據,則必須以O_WRONLY或O_RDWR的模式打開,否則無法成功寫入文件。下面是一個以os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC模式打開文件,且權限為0666向指定寫入數據的例子
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件打開成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := file.WriteString("hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}由於是以os.O_APPEND模式打開的文件,所以在寫入文件時會將數據添加到文件尾部,執行完畢後文件內容如下
hello world!
hello world!
hello world!
hello world!
hello world!向文件寫入字節切片也是類似的操作,就不再贅述。對於寫入文件的操作標准庫同樣提供了方便函數,分別是os.WriteFile與io.WriteString
os.WriteFile
func WriteFile(name string, data []byte, perm FileMode) error使用例子如下
func main() {
err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
if err != nil {
fmt.Println(err)
}
}此時文件內容如下
hello world!io.WriteString
func WriteString(w Writer, s string) (n int, err error)使用例子如下
func main() {
file, err := os.OpenFile("README.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("文件訪問異常")
} else {
fmt.Println("文件打開成功", file.Name())
for i := 0; i < 5; i++ {
offset, err := io.WriteString(file, "hello world!\n")
if err != nil {
fmt.Println(offset, err)
}
}
fmt.Println(file.Close())
}
}hello world!
hello world!
hello world!
hello world!
hello world!函數os.Create函數用於創建文件,本質上也是對OpenFile的封裝。
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}WARNING
在創建一個文件時,如果其父目錄不存在,將創建失敗並會返回錯誤。
復制
對於復制文件而言,需要同時打開兩個文件,第一種方法是將原文件中的數據讀取出來,然後寫入目標文件中,代碼示例如下
func main() {
// 從原文件中讀取數據
data, err := os.ReadFile("README.txt")
if err != nil {
fmt.Println(err)
return
}
// 寫入目標文件
err = os.WriteFile("README(1).txt", data, 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("復制成功")
}
}*os.File.ReadFrom
另一種方法是使用os.File提供的方法ReadFrom,打開文件時,一個只讀,一個只寫。
func (f *File) ReadFrom(r io.Reader) (n int64, err error)使用示例如下
func main() {
// 以只讀的方式打開原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只寫的方式打開副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 從原文件中讀取數據,然後寫入副本文件
offset, err := target.ReadFrom(origin)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("文件復制成功", offset)
}這種復制方式需要先將源文件的全部內容讀取到內存中,再寫入目標文件,文件特別大的時候不建議這麼做。
io.Copy
另一種方法就是使用io.Copy函數,它則是一邊讀一邊寫,先將內容讀到緩沖區中,再寫入到目標文件中,緩沖區默認大小為 32KB。
func Copy(dst Writer, src Reader) (written int64, err error)使用示例如下
func main() {
// 以只讀的方式打開原文件
origin, err := os.OpenFile("README.txt", os.O_RDONLY, 0666)
if err != nil {
fmt.Println(err)
return
}
defer origin.Close()
// 以只寫的方式打開副本文件
target, err := os.OpenFile("README(1).txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
fmt.Println(err)
return
}
defer target.Close()
// 復制
written, err := io.Copy(target, origin)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(written)
}
}你也可以使用io.CopyBuffer來指定緩沖區大小。
重命名
重命名也可以理解為移動文件,會用到os包下的Rename函數。
func Rename(oldpath, newpath string) error示例如下
func main() {
err := os.Rename("README.txt", "readme.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("重命名成功")
}
}該函數對於文件夾也是同樣的效果。
刪除
刪除操作相較於其他操作要簡單的多,只會用到os包下的兩個函數
// 刪除單個文件或者空目錄,當目錄不為空時會返回錯誤
func Remove(name string) error
// 刪除指定目錄的所有文件和目錄包括子目錄與子文件
func RemoveAll(path string) error使用起來十分的簡單,下面是刪除目錄的例子
func main() {
// 刪除當前目錄下所有的文件與子目錄
err := os.RemoveAll(".")
if err != nil {
fmt.Println(err)
}else {
fmt.Println("刪除成功")
}
}下面是刪除單個文件的例子
func main() {
// 刪除當前目錄下所有的文件與子目錄
err := os.Remove("README.txt")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("刪除成功")
}
}刷新
os.Sync這一個函數封裝了底層的系統調用Fsync,用於將操作系統中緩存的 IO 寫入落實到磁盤上
func main() {
create, err := os.Create("test.txt")
if err != nil {
panic(err)
}
defer create.Close()
_, err = create.Write([]byte("hello"))
if err != nil {
panic(err)
}
// 刷盤
if err := create.Sync();err != nil {
return
}
}文件夾
文件夾的許多操作都與文件操作類似
讀取
對於文件夾而言,打開方式有兩種,
os.ReadDir
第一種方式是使用os.ReadDir函數
func ReadDir(name string) ([]DirEntry, error)func main() {
// 當前目錄
dir, err := os.ReadDir(".")
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dir {
fmt.Println(entry.Name())
}
}
}*os.File.ReadDir
第二種方式是使用*os.File.ReadDir函數,os.ReadDir本質上也只是對*os.File.ReadDir的一層簡單封裝。
// n < 0時,則讀取文件夾下所有的內容
func (f *File) ReadDir(n int) ([]DirEntry, error)func main() {
// 當前目錄
dir, err := os.Open(".")
if err != nil {
fmt.Println(err)
}
defer dir.Close()
dirs, err := dir.ReadDir(-1)
if err != nil {
fmt.Println(err)
} else {
for _, entry := range dirs {
fmt.Println(entry.Name())
}
}
}創建
創建文件夾操作會用到os包下的兩個函數
// 用指定的權限創建指定名稱的目錄
func Mkdir(name string, perm FileMode) error
// 相較於前者該函數會創建一切必要的父目錄
func MkdirAll(path string, perm FileMode) error示例如下
func main() {
err := os.Mkdir("src", 0666)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("創建成功")
}
}復制
我們可以自己寫函數遞歸遍歷整個文件夾,不過filepath標准庫已經提供了類似功能的函數,所以可以直接使用,一個簡單的文件夾復制的代碼示例如下。
func CopyDir(src, dst string) error {
// 檢查源文件夾的狀態
_, err := os.Stat(src)
if err != nil {
return err
}
return filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// 計算相對路徑
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
// 拼接目標路徑
destpath := filepath.Join(dst, rel)
// 創建文件夾
var dirpath string
var mode os.FileMode = 0755
if info.IsDir() {
dirpath = destpath
mode = info.Mode()
} else if info.Mode().IsRegular() {
dirpath = filepath.Dir(destpath)
}
if err := os.MkdirAll(dirpath, mode); err != nil {
return err
}
// 創建文件
if info.Mode().IsRegular() {
srcfile, err := os.Open(path)
if err != nil {
return err
}
// 一定要記得關閉文件
defer srcfile.Close()
destfile, err := os.OpenFile(destpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer destfile.Close()
// 復制文件內容
if _, err := io.Copy(destfile, srcfile); err != nil {
return err
}
return nil
}
return nil
})
}filepath.Walk會遞歸遍歷整個文件夾,在過程中,遇到文件夾就創建文件夾,遇到文件就創建新文件並復制,代碼相比復制文件有點多但算不上復雜。
