Skip to content

文件

Go 語言提供文件處理的標准庫大致以下幾個:

  • os庫,負責 OS 文件系統交互的具體實現
  • io庫,讀寫 IO 的抽象層
  • fs庫,文件系統的抽象層

本文會講解如何通過 Go 語言來進行基本的文件處理。

打開

常見的兩種打開文件的方式是使用os包提供的兩個函數,Open函數返回值一個文件指針和一個錯誤,

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

後者OpenFile能夠提供更加細粒度的控制,函數Open就是對OpenFile函數的一個簡單封裝。

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

先來介紹第一種使用方法,直接提供對應的文件名即可,代碼如下

go
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包也為此提供了方便函數,修改後的代碼如下

go
func main() {
  file, err := os.Open("README.txt")
  if os.IsNotExist(err) {
    fmt.Println("文件不存在")
  } else if err != nil {
    fmt.Println("文件訪問異常")
  } else {
    fmt.Println("文件讀取成功", file)
  }
}

再次運行輸出如下

文件不存在

事實上第一種函數讀取的文件僅僅只是只讀的,無法被修改

go
func Open(name string) (*File, error) {
  return OpenFile(name, O_RDONLY, 0)
}

通過OpenFile函數可以控制更多細節,例如修改文件描述符和文件權限,關於文件描述符,os包下提供了以下常量以供使用。

go
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  // 當打開的時候截斷可寫的文件
)

關於文件權限的則提供了以下常量。

go
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,表示為所有人都可以對該文件進行讀寫,且不存在時會自動創建。

go
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()函數進行操作,代碼示例如下

go
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語句裡

go
defer file.Close()

讀取

當成功的打開文件後,便可以進行讀取操作了,關於讀取文件的操作,*os.File類型提供了以下幾個公開的方法

go
// 將文件讀進傳入的字節切片
func (f *File) Read(b []byte) (n int, err error)

// 相較於第一種可以從指定偏移量讀取
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

大多數情況第一種使用的較多。針對於第一種方法,需要自行編寫邏輯來進行讀取時切片的動態擴容,代碼如下

go
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
    }
  }
}

剩余邏輯如下

go
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

go
func ReadFile(name string) ([]byte, error)

使用例子如下

go
func main() {
  bytes, err := os.ReadFile("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(string(bytes))
  }
}

輸出如下

hello world!

io.ReadAll

go
func ReadAll(r Reader) ([]byte, error)

使用例子如下

go
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結構體提供了以下幾種方法以供寫入數據

go
// 寫入字節切片
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_WRONLYO_RDWR的模式打開,否則無法成功寫入文件。下面是一個以os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC模式打開文件,且權限為0666向指定寫入數據的例子

go
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模式打開的文件,所以在寫入文件時會將數據添加到文件尾部,執行完畢後文件內容如下

txt
hello world!
hello world!
hello world!
hello world!
hello world!

向文件寫入字節切片也是類似的操作,就不再贅述。對於寫入文件的操作標准庫同樣提供了方便函數,分別是os.WriteFileio.WriteString

os.WriteFile

go
func WriteFile(name string, data []byte, perm FileMode) error

使用例子如下

go
func main() {
  err := os.WriteFile("README.txt", []byte("hello world!\n"), 0666)
  if err != nil {
    fmt.Println(err)
  }
}

此時文件內容如下

txt
hello world!

io.WriteString

go
func WriteString(w Writer, s string) (n int, err error)

使用例子如下

go
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的封裝。

go
func Create(name string) (*File, error) {
   return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

WARNING

在創建一個文件時,如果其父目錄不存在,將創建失敗並會返回錯誤。

復制

對於復制文件而言,需要同時打開兩個文件,第一種方法是將原文件中的數據讀取出來,然後寫入目標文件中,代碼示例如下

go
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,打開文件時,一個只讀,一個只寫。

go
func (f *File) ReadFrom(r io.Reader) (n int64, err error)

使用示例如下

go
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。

go
func Copy(dst Writer, src Reader) (written int64, err error)

使用示例如下

go
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函數。

go
func Rename(oldpath, newpath string) error

示例如下

go
func main() {
  err := os.Rename("README.txt", "readme.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("重命名成功")
  }
}

該函數對於文件夾也是同樣的效果。

刪除

刪除操作相較於其他操作要簡單的多,只會用到os包下的兩個函數

go
// 刪除單個文件或者空目錄,當目錄不為空時會返回錯誤
func Remove(name string) error

// 刪除指定目錄的所有文件和目錄包括子目錄與子文件
func RemoveAll(path string) error

使用起來十分的簡單,下面是刪除目錄的例子

go
func main() {
  // 刪除當前目錄下所有的文件與子目錄
  err := os.RemoveAll(".")
  if err != nil {
    fmt.Println(err)
  }else {
    fmt.Println("刪除成功")
  }
}

下面是刪除單個文件的例子

go
func main() {
  // 刪除當前目錄下所有的文件與子目錄
  err := os.Remove("README.txt")
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("刪除成功")
  }
}

刷新

os.Sync這一個函數封裝了底層的系統調用Fsync,用於將操作系統中緩存的 IO 寫入落實到磁盤上

go
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函數

go
func ReadDir(name string) ([]DirEntry, error)
go
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的一層簡單封裝。

go
// n < 0時,則讀取文件夾下所有的內容
func (f *File) ReadDir(n int) ([]DirEntry, error)
go
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包下的兩個函數

go
// 用指定的權限創建指定名稱的目錄
func Mkdir(name string, perm FileMode) error

// 相較於前者該函數會創建一切必要的父目錄
func MkdirAll(path string, perm FileMode) error

示例如下

go
func main() {
  err := os.Mkdir("src", 0666)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("創建成功")
  }
}

復制

我們可以自己寫函數遞歸遍歷整個文件夾,不過filepath標准庫已經提供了類似功能的函數,所以可以直接使用,一個簡單的文件夾復制的代碼示例如下。

go
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會遞歸遍歷整個文件夾,在過程中,遇到文件夾就創建文件夾,遇到文件就創建新文件並復制,代碼相比復制文件有點多但算不上復雜。

Golang學習網由www.golangdev.cn整理維護