Golang – 檔案與 I/O 操作
讀取與寫入檔案(os、io、ioutil)
簡介
在日常開發中,檔案的讀寫是最常見的 I/O 任務。不論是寫入設定檔、讀取日誌、或是處理大量資料,都離不開對磁碟的操作。Go 語言自帶的 os、io 以及已被 ioutil 包裝的便利函式,提供了簡潔且效能良好的檔案處理介面,讓開發者能以最少的程式碼完成複雜的工作。
本篇文章將從 基本概念、實作範例、常見陷阱與最佳實踐,一步步帶領讀者掌握 Go 中的檔案 I/O。即使是剛接觸 Go 的新手,也能在閱讀完本文後,立即在專案中安全、有效地使用檔案讀寫功能。
核心概念
1. os 套件:檔案的底層操作
os 提供了最原始的檔案描述子(file descriptor)操作,包括開啟、關閉、讀寫、設定權限等。最常用的型別是 *os.File,它同時實作了 io.Reader、io.Writer、io.Closer 等介面。
1.1 開啟檔案
// os.Open 以唯讀模式開啟檔案,若檔案不存在會回傳錯誤
file, err := os.Open("data.txt")
if err != nil {
log.Fatalf("開啟檔案失敗: %v", err)
}
defer file.Close() // 確保最後一定關閉
1.2 建立或覆寫檔案
// os.Create 若檔案不存在則建立,若已存在則截斷為空檔
out, err := os.Create("output.log")
if err != nil {
log.Fatalf("建立檔案失敗: %v", err)
}
defer out.Close()
1.3 讀寫定位(Seek)
// 移動檔案指標到檔案開頭
_, err = file.Seek(0, io.SeekStart)
2. io 套件:抽象化的資料流
io 定義了最小的 I/O 介面(Reader、Writer、Closer),以及許多組合工具,如 Copy、TeeReader、MultiWriter。使用 io,我們可以在 不同來源與目的地之間自由切換,而不必關心底層是檔案、網路或記憶體。
2.1 io.Copy – 快速複製
src, _ := os.Open("source.bin")
dst, _ := os.Create("dest.bin")
defer src.Close()
defer dst.Close()
// 直接把 src 的內容寫入 dst,返回寫入的位元組數與錯誤
if _, err := io.Copy(dst, src); err != nil {
log.Fatalf("複製失敗: %v", err)
}
2.2 bufio 包裝(緩衝 I/O)
對於大量小寫入/讀取,使用 bufio.NewWriter 或 bufio.NewReader 能顯著提升效能。
w := bufio.NewWriter(out) // 包裝 *os.File
for i := 0; i < 1000; i++ {
fmt.Fprintf(w, "第 %d 行資料\n", i)
}
w.Flush() // 必須手動刷新緩衝區
3. ioutil(已棄用但仍常見)與 os 的便利函式
在 Go 1.16 之後,ioutil 大部分功能已移到 os 或 io,但舊程式碼仍大量使用。以下示範兩種寫法,讓讀者了解 新舊 API 的對應關係。
3.1 讀取整個檔案
舊版 (ioutil.ReadFile)
data, err := ioutil.ReadFile("config.json")
if err != nil {
log.Fatalf("讀檔失敗: %v", err)
}
新版 (os.ReadFile)
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatalf("讀檔失敗: %v", err)
}
3.2 寫入整個檔案
舊版 (ioutil.WriteFile)
content := []byte(`{"mode":"production"}`)
if err := ioutil.WriteFile("settings.json", content, 0644); err != nil {
log.Fatalf("寫檔失敗: %v", err)
}
新版 (os.WriteFile)
if err := os.WriteFile("settings.json", content, 0644); err != nil {
log.Fatalf("寫檔失敗: %v", err)
}
4. 完整範例彙總
以下提供 五個實用範例,涵蓋從最簡單的讀寫到緩衝與錯誤處理的完整流程。
4.1 範例一:一次讀取小檔案
package main
import (
"fmt"
"log"
"os"
)
func main() {
// 讀取整個檔案內容
data, err := os.ReadFile("hello.txt")
if err != nil {
log.Fatalf("讀檔失敗: %v", err)
}
fmt.Printf("檔案內容: %s\n", string(data))
}
重點:
os.ReadFile會一次性把檔案載入記憶體,適合 小於 10 MB 的檔案。
4.2 範例二:逐行讀取大型文字檔
package main
import (
"bufio"
"log"
"os"
)
func main() {
f, err := os.Open("large.log")
if err != nil {
log.Fatalf("開檔失敗: %v", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
lineNo := 0
for scanner.Scan() {
lineNo++
// 只顯示前 5 行示範
if lineNo <= 5 {
log.Printf("第 %d 行: %s", lineNo, scanner.Text())
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("讀取過程錯誤: %v", err)
}
}
技巧:
bufio.Scanner內部使用緩衝,能有效處理 GB 級別 的文字檔。
4.3 範例三:寫入大量資料(使用緩衝)
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
f, err := os.Create("numbers.txt")
if err != nil {
log.Fatalf("建立檔案失敗: %v", err)
}
defer f.Close()
w := bufio.NewWriter(f)
for i := 1; i <= 100000; i++ {
fmt.Fprintf(w, "%d\n", i)
}
// 必須 Flush,否則緩衝區的資料不會寫入磁碟
if err := w.Flush(); err != nil {
log.Fatalf("Flush 失敗: %v", err)
}
}
最佳實踐:大量寫入時 一定要呼叫
Flush,否則資料可能遺失。
4.4 範例四:檔案複製(使用 io.Copy)
package main
import (
"io"
"log"
"os"
)
func main() {
src, err := os.Open("source.bin")
if err != nil {
log.Fatalf("開啟來源檔失敗: %v", err)
}
defer src.Close()
dst, err := os.Create("dest.bin")
if err != nil {
log.Fatalf("建立目的檔失敗: %v", err)
}
defer dst.Close()
// 複製整個檔案
if _, err := io.Copy(dst, src); err != nil {
log.Fatalf("複製失敗: %v", err)
}
}
效能說明:
io.Copy內部會根據來源與目的的類型自動選擇最適合的緩衝大小,通常比手寫迴圈更快。
4.5 範例五:安全寫入(使用臨時檔 + 原子替換)
package main
import (
"io/ioutil"
"log"
"os"
)
func main() {
// 先寫入臨時檔
tmp, err := ioutil.TempFile("", "config-*.json")
if err != nil {
log.Fatalf("建立臨時檔失敗: %v", err)
}
defer os.Remove(tmp.Name()) // 若出錯,確保不遺留檔案
data := []byte(`{"debug":false}`)
if _, err := tmp.Write(data); err != nil {
log.Fatalf("寫入臨時檔失敗: %v", err)
}
tmp.Close()
// 原子替換目標檔案
if err := os.Rename(tmp.Name(), "config.json"); err != nil {
log.Fatalf("檔案替換失敗: %v", err)
}
}
安全性:此技巧避免了 寫入過程中斷 導致的檔案損毀,適合 設定檔、資料庫快照 等重要檔案。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 |
|---|---|---|
忘記 defer file.Close() |
檔案描述子未釋放,會導致檔案句柄耗盡 | 每次開檔後立即 defer Close(),即使在迴圈中也要小心避免過多同時開啟的檔案 |
直接使用 os.OpenFile 而未設定正確權限 |
權限錯誤會使程式在不同平台上無法寫入 | 使用 os.FileMode(如 0644)或 os.ModePerm,並根據需求選擇 `O_CREATE |
| 在大量寫入時未使用緩衝 | 每一次 Write 都會觸發系統呼叫,效能低下 |
包裝 *os.File 為 bufio.Writer,寫完後 Flush() |
使用 ioutil.ReadAll 讀取過大檔案 |
會一次性佔用大量記憶體,甚至導致 OOM | 改用 bufio.Scanner、io.Copy 或分段讀取 (Read + buffer) |
| 錯誤忽略 | 失敗的 I/O 操作往往會留下隱藏的 bug | 每一次 I/O 呼叫後都檢查 err,必要時使用 log.Fatalf 或自訂錯誤處理 |
最佳實踐總結:
- 始終檢查錯誤,不要假設檔案一定存在或寫入一定成功。
- 使用
defer關閉檔案,確保資源即時釋放。 - 針對不同大小的檔案選擇適當的 API:小檔案
os.ReadFile,大檔案bufio.Scanner或io.Copy。 - 寫入大量資料時加緩衝,並記得
Flush。 - 考慮原子寫入(臨時檔 +
os.Rename),提升資料完整性。
實際應用場景
| 場景 | 需求 | 推薦的 Go API |
|---|---|---|
| 設定檔載入 | 讀取 JSON/YAML,檔案大小通常 < 1 MB | os.ReadFile + json.Unmarshal |
| 日誌輪替 | 持續寫入、每日或檔案大小達到上限時切換 | os.OpenFile (O_APPEND) + bufio.Writer |
| 檔案上傳/下載 | 大檔案 (GB 級) 需要流式傳輸 | io.Copy 搭配 net/http Body |
| 資料備份 | 把資料庫快照寫入磁碟,必須保證完整性 | 臨時檔 + os.Rename(原子寫入) |
| 文字分析 | 逐行讀取巨量文字檔,計算關鍵字出現次數 | bufio.Scanner + map[string]int |
透過上述對應表,開發者可以快速選擇最合適的 I/O 方法,避免「過度抽象」或「過度手寫」的情況。
總結
os提供底層檔案操作,是所有 I/O 的根基。io抽象出通用的讀寫介面,讓我們可以在不同資料來源間自由切換。ioutil(雖已棄用)仍是舊程式碼的常見寫法,了解其與os/io的對應關係有助於維護與升級。
掌握 正確的錯誤處理、資源釋放與緩衝策略,即可在 Go 中寫出 安全、效能佳且易於維護 的檔案 I/O 程式。未來在處理更複雜的資料流(如網路串流、壓縮檔案、雲端儲存)時,這些基礎概念仍是不可或缺的基石。祝你在 Golang 的檔案與 I/O 世界裡玩得開心!